ECMAScript规范的第五版中,对函数的执行上下文重新进行了定义,与第三版有一定的区别。
本文第一部分对规范中定义的名词进行解释。第二部从实际的例子出发,深入解释器的处理过程。
第一部分:名词定义
Environment Record(环境记录项 RE):
用于处理标识符的绑定。一个环境记录项(RE)只记录与它相关联的词法环境(LE)中的标识符绑定。规范中定义了两种类型的环境记录项,声明式和对象式,这两种环境记录项的关系可以看成是对一个抽象的环境记录项的具体化,抽象的环境记录项中定义了一系列的抽象方法用来处理标识符的绑定,声明式和对象式的环境记录项各自实现对应的方法。
Declarative Environment Records(声明式环境记录项 DER):
规范中定义的这种环境记录项可以由解释引擎自己决定实现方式,主要就是为了提高解释器的性能(例如GoogleV8引擎中,如果嵌套函数中没有使用外部变量,那么此处的词法环境可以处理所有的绑定,也就不需要维持一个外层词法环境的引用)。函数代码中的变量声明,函数声明,catch从句一般都是使用声明式环境记录项来处理标识符绑定。
Object Environment Records (对象式环境记录项 OER):
规范中定义的这种环境记录项使用一个JavaScript对象来处理标识符绑定,可以将变量声明,函数声明作为属性值对存储在这个绑定对象上。全局环境和with语句一般使用这种环境记录项。
Lexical Environment(词法环境 LE):
词法环境由环境记录项,和一个可能为空的(Outer Lexical Environment)外部词法环境组成。这种结构在逻辑上构成了嵌套关系,在进行变量解析时,首先从内层词法环境中查找标识符,如果没有找到,再到外层查找。
Execution Contexts (执行上下文 EC):
当控制器转入ECMA脚本的可执行代码时,控制器会进入一个执行上下文。当前活动的多个执行上下文在逻辑上形成一个栈结构。该逻辑栈的最顶层的执行上下文称为当前运行的执行上下文。任何时候,当控制器从当前运行的执行上下文相关的可执行代码转入与该执行上下文无关的可执行代码时,会创建一个新的执行上下文。新建的这个执行上下文会推入栈中,成为当前运行的执行上下文。
执行上下文中包含3个组件:
- LexicalEnvironment(词法环境 LE),指定一个词法环境对象,用于解析当前执行上下文中代码创建的标识符引用
- VariableEnvironment(变量环境 VE),指定一个词法环境对象,其环境记录项用于保存由该执行环境内的代码通过变量表达式和函数表达式创建的绑定。
- ThisBinding(This绑定),保存当前执行环境中this的值
这里的词法环境和上文中讲到的词法环境概念上是不一样的,上文中的词法环境是规范中定义的一种数据结构,而这里的词法环境只是执行上下文中一种组件的名字,而且这种组件其实就是上文中的词法环境对象。
这里的词法环境和变量环境虽然名字不一样,但在大多数情况下,两者都一样,在初始化执行上文时,词法环境只是变量环境的一个拷贝。在遇到catch或者with语句块的时候,当前执行上下文中的词法环境会被替换为一个新的词法环境,在退出语句块的时候会恢复之前的词法环境。而通过函数声明语句创建函数对象时,会使用变量环境作为函数对象的[[Scope]]内部属性值,而通过函数表达式创建的函数对象则使用词法环境作为函数对象的[[Scope]]内部属性。
函数对象中与变量解析和标识符绑定相关的属性:
- [[Scope]] 用于记录定义函数对象时所在的词法环境,在进入函数的可执行代码时需要构建执行上下文,这个属性可以作为执行上下文中词法环境的外部词法环境引用,从而让词法环境在逻辑上构成嵌套关系。
- [[Code]] 保存了函数的函数体,在调用函数时,解释器就是从这里获取函数的代码的
- [[FormalParameters]] 保存了函数的形参列表,函数的每个形参都会进行标识符绑定存储在环境记录项中
- [[Call]] 函数特有的一个内部属性,它的值是一个方法。正因为这个属性才使得函数可以被调用。解释引擎遇到一个调用表达式时,会查看”()”前的表达式是否是可调用的,如果可调用,就会执行这个方法。这个方法内部逻辑一般是这样的
1.通过函数的[[Scope]]属性,一个this值构建一个新的执行上下文funcCtx,根据函数的形参,实参以及函数体进行形参,函数声明,变量声明的初始化绑定
2.result为执行函数体中的代码后的返回结果,如果函数体为空则返回结果为(normal, undefined, empty)
3.退出funcCtx执行上下文,恢复之前的执行上下文
4.如果result.type为throw,抛出result.value
5.如果result.type为return,返回result.value
6.否则result.type一定为normal,返回undefined
JavaScript中的三种代码:
规范中将代码抽象成三种类型,控制器在进入这三种代码时会有不同的逻辑。
- 全局代码
不在函数内定义的代码以及不是通过eval执行的代码,都属于全局代码。解释器在执行任何代码前,会先初始化一个全局环境,全局环境是一个唯一的词法环境,由一个对象式环境记录项和一个为null的外部词法环境引用组成,而这个对象式环境记录项绑定的对象就是全局对象。
控制器进入全局代码后,会执行以下步骤:
1.初始化执行上下文,变量环境和词法环境就是使用之前初始化好的全局环境,this绑定引用全局对象
2.扫描所有的全局代码,执行变量,函数的声明绑定
- eval代码