Javascript预解释与执行上下文Execution Context的关系

之前从YouDontKnowJS了解到了JS声明会隐式‘提升’,就是大概在预解释阶段把变量声明、函数声明等先提到作用域的顶端声明了然后在执行阶段才执行赋值等操作,最近又从这篇文章http://davidshariff.com/blog/...深入了解了一下执行上下文的原理,大概是执行前先生成一个executionContextObj

executionContextObj = {
    'scopeChain': { /* variableObject + all parent execution context's variableObject */ },
    'variableObject': { /* function arguments / parameters, inner variable and function declarations */ },
    'this': {}
}

先初始化作用域链scopeChain,然后把参数、声明的函数和变量等等传进variableObject:每找到一个函数声明,就在variableObject下面用函数名建立一个属性,属性值就是指向该函数在内存中的地址的一个引用,如果上述函数名已经存在于variableObject下,那么对应的属性值会被新的引用所覆盖;接着检查当前上下文中的变量声明,每找到一个变量的声明,就在variableObject下,用变量名建立一个属性,属性值为undefined,如果该变量名已经存在于variableObject属性中,直接跳过(防止指向函数的属性的值被变量属性覆盖为undefined),原属性值不会被修改。最后this指向调用函数的对象。然后再开始执行,然后执行到其他函数调用时又执行一次上述过程如此类推。
就是这段,特别是variableObject的传值过程让我觉得非常像预解释中的提升,是否可以说这就是造成执行前声明提升的原因,或者说创建executionContextObj的过程可以理解为预解释?

阅读 4.4k
3 个回答

你的理解是正确的,答案已经说出来了。以下只能补充一下而已。

变量、函数(主要是用于FD)、类都有提升的特性。这是因为在执行的过程,也就是你所说的执行上下文(EC)在建立的过程时,就有一个阶段是称为绑定(binding)的步骤,在这步骤时会寻找目前要建立的这个上下文(EC)的变量、函数、类等资讯,这一段在ECMAScript标准中,有一节称为Declaration Binding Instantiation(宣告绑定初始化),其中就有提到它的规则,简单翻一段:


每个执行上下文(EC)都会有个关联的VariableEnvironment(变量环境)。被评估包含在执行上下文(EC)中的在ECMAScript代码里所定义的变量与函数,会被加入到VariableEnvironment(变量环境)的Environment Record(环境记录)里成为绑定。对于函数的代码来说,传参也会加入到Environment Record(环境记录)成为绑定。


Environment Record(环境记录)就是执行上下文(EC)用来绑定声明的记录部份,其中又分两类…。所以在JS引擎执行时,会先作声明部份的绑定,这个步骤时会对已经存在记录里的声明进行扫描,如果已有记录的,新的声明会覆盖掉已有的。

下面是绑定步骤的一个的代码示例,代码参考自这里:

var foo;

(function(){
    foo = 1;    // 此步骤时,没有要绑定的
    var foo;    // 清除之前绑定的,然后新建一个声明,指定为undefined
    foo = 2;    // 此步骤时,没有要绑定的
})();

上面所说的是执行上下文的三大部份中的VariableEnvironment,而作用域是属于LexicalEnvironment,可以参考标准中的说明

再补充一点是,以V8引擎的源代码来看,JS代码在执行前的确是有经过编译过程的,然后最后才在电脑中执行。只是通常以它对开发的执行过程而言,会称它是"直译"执行,而不是"编译为可执行档"的那种执行。

最后,会被提升的目前有下面这些,不过提升仍有一些细节上的不同,请再找其他资料了:

var, let, const, function, function*, class


补充与回复@m2mbob的评论

我说的这几个都会被提升,但是都有一些细节,以下用let, const来说明,它们是会被提升(hoisted)的,但因为它们被定义有一段时间是无法存取的,在被声明与进入作用域之间时,这段时间称为temporal dead zone(TDZ),所以不同于var或function,存取let或const的提升的变量会报错ReferenceError,而不是undefined

要理解let, const是否会被提升,可以用下面的简单例子来看:

第一个例子,是正常的使用,可以输出x变量的值在控制台:

let x = 'outer scope';

(function() {
    console.log(x);
}());

第二个例子,报错。这是因为函数中的那个x变量被提升到函数中区块的最上面,因此造成错误:

let x = 'outer scope';

(function() {
    console.log(x);
    let x = 'inner scope'; //多加这行代码
}());

这里的相关说明可以再参考这里的问答,与上面这个例子的出处

根据es 规范简单描述整个过程:

  1. 函数调用(确定this和arguments),函数调用的最后一步会调用内部方法 [[Call]]

  2. 内部方法[[Call]]的第一步就是建立函数执行上下文,也就是你所说的 executionContextObj

  3. 执行上下文的建立包括了参数绑定及初始化、函数声明绑定及初始化、变量声明绑定,也就是你所说的预解析。

  4. 然后再回到内部方法 [[Call]],执行函数,然后返回结果。

我的回答:并不是!函数提升和变量对象创建是两码事。函数提升的理解可以从JavaScript的函数没有重载理解,也就是对于同名函数不区分,这可以理解为代码执行前的准备。变量对象创建是在代码执行过程由外层函数到内层函数层层创建(你给的参考文献里已经说得很明白了)。作用的时间不同,怎么能理解成一样呢?

推荐问题
宣传栏