执行上下文的结构
这部分主要讨论在ES5中执行上文的结构,它与ES3中的有一些不同,先看看它的组成组件:
ExecutionContextES5 = {
ThisBinding: <this value>,
VariableEnvironment: { ... },
LexicalEnvironment: { ... },
}
在执行上下文中同时包含词法环境组件和变量环境组件(译注:这里的词法环境和之前讨论的不同,为了区分,我这里称呼为词法环境组件),对于这一点经常让那些阅读规范的人感到困惑。我们很快会弄清楚它们,但在这先简单的说下它们的区别在于函数声明(FD)和函数表达式(FE)中[[scope]]值的不同。
下面来分别看下执行上下文中的组成。
this绑定
this值现在改名为做this绑定,虽然称呼术语换了,但在语法角度上来说没有太大区别(除了在严格模式下,this值可以是undefined)。在全局上下文中,this绑定仍然是全局对象。
(function (global) {
global.a = 10;
})(this);
console.log(a); // 10
在函数内部的执行上下文中,this值仍然是由函数被调用的方式所决定的。被调用时生成一个引用类型(Reference type),而引用类型的base属性的值就是this值。这种判定方法在所有情况下都使用,无论是针对全局对象或者严格模式下值为undefined的this绑定。
var foo = {
bar: function () {
console.log(this);
};
};
// --- Reference cases ---
// with a reference
foo.bar(); // "this" is "foo" - the base
var bar = foo.bar;
// with the reference
bar(); // "this" is the global, implicit base
this.bar(); // the same, explicit base, the global
// with also but another reference
bar.prototype.constructor(); // "this" is "bar.prototype"
// --- non-Reference cases ---
(foo.bar = foo.bar)(); // "this" is "global" or "undefined"
(foo.bar || foo.bar)(); // "this" is "global" or "undefined"
(function () { this; })(); // "this" is "global" or "undefined"
注意,下面这种方式在严格模式下已无法获得全局对象:
(function () {
"use strict";
var global = (function () { return this; })();
console.log(global); // undefined!
})();
关于这部分的具体内容可以在严格模式(注二)一章中找到。
现在让我们回到环境中,看看上下文中变量环境组件和词法环境组件在规范中的含义。这两个组建经常会引起混淆,而且不幸的是在某些解释中对二者的描述是错误的。
变量环境组件VariableEnvrionment
确切地说 ,变量环境组件是上下文中变量声明和函数声明的初始储存位置。确切的说,变量环境组件等价于一个词法环境,该环境的环境记录项拥有在当前上下文中创建的变量声明绑定。这一点很像ES3中的变量对象
当进入函数的执行上下文时,创建一个特殊的参数对象(arguments object),里面储存了形参的值(译注:储存函数的形式参数值,按参数列表从左到右排列)。
但现在在严格模式下,参数对象经历了多次更改。其中arguments属性不再与函数的实际参数之间共享,同时callee属性(对函数本身的引用)也被移除。
看下面代码:
function foo(a) {
var b = 20;
}
foo(10);
下面是foo函数上下文变量环境的抽象结构:
fooContext.VariableEnvironment =
{
environmentRecord: {
arguments: {0: 10, length: 1, callee: foo},
a: 10,
b: 20
},
outer: globalEnvironment
};
(译注:
此外在严格模式下:
"use strict";
(function foo(x) {
alert(arguments[0]); // 10
arguments[0] = 20;
alert(x); // 10, but not 20
x = 30;
alert(arguments[0]); // 20, but not 30
})(10);
)
那什么是词法环境呢?有意思的一点是,其词法环境组件和变量环境组件最初是同一个值,词法环境组件复制于变量环境,变量环境组件永远不会变,而词法环境组件可能会变。
词法环境组件LexicalEnvrionment
执行上下文的变量环境组件还是词法环境组件都是属于词法环境的概念(先不用管这里名字上造成的困惑)(译注:可以看作它们是执行上下文的组成,类型都是词法环境,但在这里作为执行上下文的两种不同组成,这么称呼),都是在静态的(词法的)决定当前上下文中创建的内部函数的外部绑定。
我们上面提到过,在初始化阶段(刚进入上下文,上下文被激活),词法环境组件就是变量环境组件的一个副本,就以上面这个例子来看:
fooContext.LexicalEnvironment = copy(fooContext.VariableEnvironment);
词法环境组件的改变发生在代码执行阶段,与with语句和catch从句对词法环境的扩张有关(不过,我们说过ES5中已经改为替换当前上下文环境,而非ES3中那样去扩张当前上下文环境).。
在上面讲过,with语句和catch语句会在它们的代码执行阶段替代掉当前的执行上下文环境,而一点与函数表达式有着密切关系。
我们知道,在函数创建阶段,会创建一个闭包来保存它创建时所在的那个词法环境。
如果一个函数表达式是创建在with语句(或catch语句)中,它同样应该保存当前的(新创建的这个with环境)词法环境。
按照规则,用with创建的环境替代了当前变量环境组件后,当with执行完成,应该把之前的变量环境恢复过来。然而这也意味着,此时函数表达式将无法访问到在with执行时创建的绑定,但函数表达式的确需要这些with内绑定。
而且,(with语句执行阶段所新创建的环境)不能去替换掉当前的变量环境,因为函数声明同样可以在with语句中被调用,并且不同于函数表达式,函数声明应该继续使用它创建时所在词法环境的绑定值而不是来自with对象(下面会有一个例子)。
所以这也是为什么,作为函数声明形成的闭包会将变量环境作为它[[scope]]属性的值。而在这个例子中对于函数表达式,它会保存with中创建的词法环境组件作为[[scope]]属性的值。这一点是最主要,也是唯一点来区分这两个概念的不同之处。
而要是with语句完全的从ES中消失(可能在下个版本的ES),或正如目前ES5的严格模式下,那么ES概念在这方面的困惑就会少很多。
再次说明,函数表达式(FE)保存的是词法环境组件,因为它需要动态绑定在with执行时创建的环境。而函数声明(FD)保存的是变量环境组件,根据规范它不能被创建于块级作用域中,并且由于采用声明提升的规则。通过下面这个例子来具体了解它们的内部过程:
var a = 10;
// FD
function foo() {
console.log(a);
}
with ({a: 20}) {
// FE
var bar = function () {
console.log(a);
};
foo(); // 10!, 来自变量环境组件
bar(); // 20, 来自词法环境组件
}
foo(); // 10
bar(); // still 20
它的过程是这样:
// "foo" is created
foo.[[Scope]] = globalContext.[[VariableEnvironment]];
// "with" is executed
previousEnvironment = globalContext.[[LexicalEnvironment]];
globalContext.[[LexicalEnvironment]] = {
environmentRecord: {a: 20},
outer: previousEnvironment
};
// "bar" is created
bar.[[Scope]] = globalContext.[[LexicalEnvironment]];
// "with" is completed, restore the environment
globalContext.[[LexicalEnvironment]] = previousEnvironment;
为了更清楚的了解这个过程,我们来写些不怎么符合规范的代码,把函数声明移到块中,要记住这是个语法错误。但其实在今天所有的引擎实现都不会抛出错误,而是有一套自己的机制来管理这种情况。Firefox 就有一套非标准的扩展实现叫做函数语句(FS)。其他实现,比如google的V8,会有下面的表现。
var a = 10;
with ({a: 20}) {
// FD
function foo() { // do not test in Firefox!
console.log(a);
}
// FE
var bar = function () {
console.log(a);
};
foo(); // 10!, from VariableEnvrionment
bar(); // 20, from LexicalEnvrionment
}
foo(); // 10
bar(); // still 20
这里同样是上面提到的情况,函数声明(这里存在声明提升)保存变量环境组件作为[[scope]]的值,而函数表达式保存的with执行时被替换后的那个词法环境组件,即with创建的环境。
注意:ES6中规范了块级函数声明,所以在上面这个例子中,foo会采用创建时所在的词法环境,输出20(译注:注意这里说的是词法环境,因为存在块级作用域,所以块内形成了一个新的词法环境)。
虽然我们从抽象的角度讨论了两种环境,但是在抽象的定义和讨论中,并不会进行严格的区分。所以我们可能会经常说执行上下文的词法环境。
然而,有一点需要注意,词法环境组件会参与标识符解析的过程。
下一节见:ECMA-262-5 词法环境:ECMA实现(四)— 标识符解析及其他
原文链接
ECMA-262-5 in detail. Chapter 3.2. Lexical environments: ECMAScript implementation.