首先我们先看一副图
准确的说js的编译阶段和执行阶段不像传统的编译型语言,并没有分的很清楚。
在程序中我们把语言分为解释型语言与编译型语言。
解释型语言:在运行的时候将程序翻译成机器语言,所以运行速度相对于编译型语言要慢。
编译型语言:编译型语言在程序执行之前,有一个单独的编译过程,将程序翻译成机器语言,以后执行这个程序的时候,就不用再进行翻译了。
尽管 JavaScript 一般被划分到“动态”或者“解释型”语言的范畴,但是其实它是一个编译型语言。它 不是 像许多传统意义上的编译型语言那样预先被编译好,编译的结果也不能在各种不同的分布式系统间移植。
传统的编译过程大致分为:
- 分词/词法分析: 将一连串字符打断成(对于语言来说)有意义的片段
- 解析: 生成抽象语法树
- 代码生成:将抽象语法树转换为计算机可识别的可执行的代码
但是JS与其他语言不同,JavaScript 的编译过程不是发生在构建之前的。
对于 JavaScript 来说,大部分情况下编译发生在代码执行前的几微秒(甚至更短!)的时 间内。任何 JavaScript 代码片段在执行前都要进行编译(通常就在执行前)。因此, JavaScript 编译器首先会对 var a = 2; 这段程序进行编译,然后做好执行它的准备,并且 通常马上就会执行它。(我认为这里的编译就是执行上下文的进入执行上下文(创建)阶段,因为编译往往就在代码执行前进行的。js引擎一般会对代码的执行进行大量的优化工作例如JIT
JavaScript 引擎执行的步骤更为复杂。
作用域
定义如何在某些位置存储变量,以及如何在稍后找到这些变量。我们称这组规则为作用域。是通过标识符名称查询变量的一组规则。作用域是在代码编译阶段确定的,一旦代码写好,它的作用域就确定了。js中只有两种作用域,即全局作用域和局部作用域。
代码一旦写好, 不用执行, 作用范围就已经确定好了。但真正能访问到的的值是什么还需要执行上下文进入创建时候确认,就是把规则转换为具体的对象。
var a = 2
解析这段简单的代码的过程是什么样的呢?
编译器先进行词法分析。但是这个编译器不是单单的自己工作,它需要结合作用域一起。
-
遇到 var a,编译器 让 作用域 去查看对于这个特定的作用域集合,变量 a 是否已经存在了。如果是,编译器 就忽略这个声明并继续前进。否则,编译器 就让 作用域 去为这个作用域集合声明一个称为 a 的新变量。
-
然后 编译器 为 引擎 生成稍后要执行的代码,来处理赋值 a = 2。引擎 运行的代码首先让 作用域 去查看在当前的作用域集合中是否有一个称为 a 的变量可以访问。如果有,引擎 就使用这个变量。如果没有,引擎 就查看 其他地方(参见下面的嵌套 作用域 一节)。
当 引擎 执行 编译器 在第二步为它产生的代码时,它必须查询变量 a 来看它是否已经被声明过了,而且这个查询是咨询 作用域 的。但是 引擎 所实施的查询的类型会影响查询的结果。
在我们这个例子中,引擎 将会对变量 a 实施一个“LHS”查询。另一种类型的查询称为“RHS”。RHS 是难以察觉的,因为它简单地查询某个变量的值,而 LHS 查询是试着找到变量容器本身,以便它可以赋值。
作用域就像大楼一样会有作用域链的嵌套(执行上下文会产生作用域链)。
变量对象
我们首先看一下什么是变量对象
如果变量与执行上下文相关,那变量自己应该知道它的数据存储在哪里,并且知道如何访问。这种机制称为变量对象。
定义几乎和作用域一样,那这两者有什么区别呢?
我的理解为:作用域是一个抽象的概念,变量对象是对抽象概念实现的一个实实在在的对象。上面提到的作用域是一个抽象的概念,js中并没有实际的一个作用域去配合引擎执行代码。只是一套规则。
VO 和 AO
- VO (Variable Object)变量对象,对应的是函数创建阶段,JS解析引擎进行预解析时,所有变量和函数的声明(即在JS引擎的预解析阶段,就确定了VO的内容,只不过此时大部分属性的值都是undefined)。存储着:
(1)变量 (var, 变量声明); // 只有通过var声明的才会进入变量对象 a=1 这种不会
(2)函数声明 (FunctionDeclaration, 缩写为FD);
(3)函数的形参 - AO(Activation Object)活动对象,对应的是函数执行阶段,对VO的属性进行赋值。
VO是不可以被访问的,AO是可以被访问的。
AO 实际上是包含了 VO 的。因为除了 VO 之外,AO 还包含函数的 parameters,以及 arguments 这个特殊对象。
AO = VO + function parameters + argumentss
执行上下文
执行上下文可以理解为当前代码的执行环境。是在函数执行时候产生的。以栈的方式,当Javascript解释器初始化执行代码时,它首先默认进入一个全局执行上下文。在此基础上每一次函数的调用都将创建一个新的执行上下文。
每次一个新的执行上下文被创建时,它都被添加到了执行栈的顶部。浏览器总是执行当前位于执行栈顶部的执行上下文。一旦执行完成,它就会被从栈的顶部移除,并将控制权返回到它下面的执行上下文。
如何产生执行上下文
- 全局代码
- 函数代码
- eval代码
可以将每个执行上下文抽象为一个对象并有三个属性。
executionContextObj = {
scopeChain: { /* 变量对象(variableObject)+ 所有父执行上下文的变量对象*/ }, // 作用域链
variableObject: { /*函数 arguments/参数,内部变量和函数声明 */ }, // 变量对象
this: {}
}
scopeChain:作用域链。函数执行上下文对象产生的作用域链。
那么作用域链是什么呢?函数作用域链 = 活动对象(AO) + scope属性
scope属性是什么呢?
scope属性是静态的,代码写好时候就是确定的,scope为他们外层作用域也就是外层变量(该函数对象的所有父变量对象的)对象(VO)的集合。执行上下文会复制函数 [[scope]] 属性创建作用域链。函数的作用域链,是函数执行的时候动态创建的,但是它又是基于静态词法的环境(scope属性)这就是为什么作用域是静态的,作用域链一般都是动态的原因动静结合。
执行上下文一般分为两个阶段
- 创建(进入执行上下文)阶段 // 应该是一个很短暂的瞬间,函数被调用但是内部没有执行就为创建阶段
- 执行阶段
如最上面那副图所示
创建阶段(当函数被调用,但未开始执行内部代码之前)
- 创建 Scope Chain
- 创建VO/AO (函数内部变量声明、函数声明、函数参数) // 变量提升存在这里
- 设置this值
激活阶段/代码执行阶段
- 设置变量的值、函数的引用,然后解释/执行代码。
为什么变量提升存在于执行上下文创建阶段而不是编译阶段?
编译阶段只是把代码生成计算机可识别的代码,计算机真正去理解代码还是需要执行。
执行上下文概念
执行上下文实例
汤姆大叔作用域链(Scope Chain)
了解执行上下文
了解完概念后试着用上下文的概念解释一个问题
网友评论