本文译自Dmitry Soshnikov的《ECMA-262-3 in detail》系列教程。其中会加入一些个人见解以及配图举例等等,来帮助读者更好的理解JavaScript。
声明:本文不涉及与ES6相关的知识。
前言
在本系列教程上一篇文章《从ECMAScript规范深度分析JavaScript(二):变量对象(上)》中我们讲述了变量对象的概念,以及在不同上下文中变量对象的区别,接下来让我们来深入探讨一下,在程序的不同阶段变量对象的行为、关于变量所要知道的细节以及特殊属性__parent__。
处理上下文代码的阶段
现在我们终于触及到本文的核心内容,执行上下文的代码被分成两个基本的阶段来处理:
- 进入执行期上下文
- 代码执行
变量对象的变化与这两个阶段紧密相关。
注意:这两个阶段不管是全局还是函数上下文的共同行为。
1、进入执行期上下文
在进入执行期上下文还没开始执行代码时,VO中有以下属性(在之前已经阐述过了):
- 所有的形参(如果是在函数的执行期上下文中)
这是变量对象的一个由形参的名和值创建的属性;如果没有对应传递实际参数,那么这个属性就由形式参数的名称和undefined值创建 - 所有的函数声明
这是变量对象的一个由函数对象的名和值创建的属性;如果变量对象已经包含了一个相同的属性名称,那么将会替换掉他的值和特性 - 所有的变量声明
这是变量对象的一个由变量名和值创建的一个属性;如果变量名已经和一个形参或者是一个函数名相同,则变量声明不会改变已经存在的属性
我们来看个例子:
function test(a, b) {
var c = 10;
function d() {}
var e = function _e() {};
(function x() {});
}
test(10); // 调用
在传递10这个参数进入test函数上下文时,AO像下面这样
AO(test) = {
a: 10,
b: undefined,
c: undefined,
d: <reference to FunctionDeclaration "d">
e: undefined
};
注意:AO并不包含函数x,这是因为x不是函数声明而是函数表达式(Function-Expression,缩写为FE),而函数表达式不会影响变量对象。
然而,函数_e也是一个函数表达式,但是我们接下来会发现,因为将它赋值给了变量e,我们可以通过变量名e来访问它(我们暂时先不讨论函数声明和函数表达式的区别)。
在这之后,就来到了上下文代码进行的第二阶段,代码执行期。
2、代码执行
此时,AO/VO已经充满了属性(但是,它们不是所有的属性都有真实值,其中的大多数仍然是初始值undefined)。
思考一下同样的例子,AO/VO在代码解释执行期间被修改:
AO['c'] = 10;
AO['e'] = <reference to FunctionExpression "_e">;
我们再次注意到,函数表达式_e仍然只存在于内存中,因为它被存储给了变量声明e,但是函数表达式x不存在于AO/VO中。如果我们尝试在定义前(甚至是定以后)调用x函数,我们将会得到一个错误:“x” is not defined。
注意:没有保存的函数表达式只能在他定义的地方或者递归中调用。
一个经典的例子:
alert(x); // 是个函数
var x = 10;
alert(x); // 10
x = 20;
function x() {}
alert(x); // 20
为什么第一次打印x是函数并且还是在定义它之前访问的?为什么不是10或者20呢?
因为: 根据规则,VO对象在进入上下文的阶段填入函数声明了(这个常被称为函数声明提前);在同一阶段,还有一个变量声明“x”,那么正如我们之前提及的那样,变量声明在顺序上跟在函数声明和形式参数声明之后,而且,在这个阶段(指进入执行上下文阶段),变量声明不会干扰VO中已经存在的同名函数声明或形式参数声明,因此,在进入上下文时,VO的结构如下:
VO = {};
VO['x'] = <reference to FunctionDeclaration "x">
// 发现var x = 10;如果函数x不是已经定义了,x将会是undefined,但在我们的示例中,变量声明没有影响到具有相同名称函数的值。
VO['x'] = <the value is not disturbed, still function>
之后,我们进入到代码执行阶段,VO的变化如下:
VO['x'] = 10;
VO['x'] = 20;
我们将会在第二次和第三次打印中看到这些。
在下面这个例子中,我们会再次发现,变量是在进入上下文阶段放入VO的(else代码块从未执行,但是没用,变量b依然存在于VO当中):
if (true) {
var a = 1;
} else {
var b = 2;
}
alert(a); // 1
alert(b); // undefined, 而不是"b is not defined",证明b是声明过了的
注:这是因为在ES6之前,javascript没有块级作用域的概念,ES6中情况会有其他情况(比如let声明),我们在这里不作讨论。
关于变量
通常,各类文章甚至JavaScript书籍都声称:“可以在全局作用域中使用var关键字或者在所有地方不使用var关键字来声明全局变量”。事实上并不是这样,一定要记住:
任何时候,变量只能通过使用var关键字才能声明。
向下面这样赋值:
a = 10;
只是创建了全局对象的一个属性,而不是变量。“不是变量”不是说它不能够被改变,而是它不符合ECMAScript的概念(他们也变成了全局对象的属性,因为 VO(globalContext) === global,这个我们在之前就有提及)。
我们来看一下这个例子来了解他们的区别:
alert(a); // undefined
alert(b); // "b" is not defined
b = 10;
var a = 20;
所有这些都取决于VO和他的修改阶段(进入上下文阶段和代码执行阶段)
进入上下文时:
VO = {
a: undefined
};
我们发现在这个阶段没有b,因为它不是变量,b将只会在代码执行阶段出现(但是在我们的代码中没有,因为会有报错)
改一下代码:
alert(a); // undefined, 我们知道这种情况
b = 10;
alert(b); // 10, 在代码执行阶段被创建
var a = 20;
alert(a); // 20, 在代码执行阶段被修改
这里关于变量还有一个更重要的点,和普通简单属性不同,变量有{DontDelete}特性,这意味着没法通过delete运算符删除变量:
a = 10;
alert(window.a); // 10
alert(delete a); // true
alert(window.a); // undefined
var b = 20;
alert(window.b); // 20
alert(delete b); // false
alert(window.b); // still 20
但是这条规则在一种执行期上下文的情况下不生效,即eval上下文中{DontDelete}特性不会被设置给变量:
eval('var a = 10;');
alert(window.a); // 10
alert(delete a); // true
alert(window.a); // undefined
所以有些人使用某些debug调试工具的控制台测试这个例子时会出现问题,比如Firebug:
注意:Firebug使用的是也是eval来执行你控制台的代码的,所以这里的变量没有{DontDelete}特性,进而能够被删除。
特殊实现:__parent__属性
我们之前已经提到,根据标准,是没法直接获取激活对象的。但是,在一些实现上没有遵守这个标准,比如SpiderMonkey 和Rhino中,函数拥有一个特殊的**parent**属性,用来指向创建这个函数的激活对象(或者是全局变量对象)。
示例(SpiderMonkey,Rhino):
var global = this;
var a = 10;
function foo() {}
alert(foo.__parent__); // global
var VO = foo.__parent__;
alert(VO.a); // 10
alert(VO === global); // true
在上面的例子中,foo函数在全局上下文中被创建,因此,他的__parent__属性被设置为全局上下文的变量对象(也就是全局对象)。
然而,没法用相同的方式从SpiderMonkey中访问激活对象:根据版本的不同,内部函数的__parent__属性返回null或者全局对象。
在Rhino中是允许通过同样的方式来来访问激活对象的,比如:
var global = this;
var x = 10;
(function foo() {
var y = 20;
// the activation object of the "foo" context
var AO = (function () {}).__parent__;
print(AO.y); // 20
// 当前激活对象的__parent__已经存在是全局对象
// 这样特殊的变量对象链就形成了,就是作用域链
print(AO.__parent__ === global); // true
print(AO.__parent__.x); // 10
})();
结语
在本章中我们进一步学习了和执行期上下文相关的对象,此篇是很重要的一章,明白了这其中的机制,我们才能够对后续的作用域链,闭包等知识有更好的理解。
希望此文能够解决大家工作和学习中的一些疑问,避免不必要的时间浪费,有不严谨的地方,也请大家批评指正,共同进步!
转载请注明出处,谢谢!
交流方式:QQ1670765991