ECMA-262-3深入解析第二章:变量对象

从来没有深入了解ECMA,网上找了一下,发现早在2010年就有大佬 Dmitry Soshnikov 总结了ECMA中的核心内容,我这里只是翻译记录,加深自己的印象。文章原文来自 ECMA-262-3 in detail. Chapter 2. Variable object.

介绍

在我们创建应用程序的时候,总是避免不了会进行函数和变量的声明。但是,解释器(interpreter)是怎么找到我们的数据(函数和变量)的呢?又是在哪里找到的呢?我们引用我们需要的对象的时候又发生了什么呢?

大部分程序员都知道变量与执行上下文紧密相关。

1
2
3
4
5
6
7
8
var a = 10; // variable of global context

(function foo() {
var b = 20; // local variable of the function context
})();

alert(a); // 10
alert(b); // b is not defined

同样,许多程序员也都知道,在当前的版本规范中,只有函数(function)代码的执行上下文才可以创建独立的作用域。与C++等语言相反,在ECMAScript中,for循环 没有 创建一个独立的作用域。这就是为什么,下面的代码, i 始终是5

1
2
3
4
5
6
var obj = {};
for (var i = 0; i < 5; i++) {
obj[i] = function() {console.log(i)}
}

for (var k in obj) { obj[k]() }

我们来详细了解一下声明数据的时候都发生了什么。

数据声明

如果变量与执行上下文相关,那么,他就知道它的数据存放在哪里以及如何获取。这种机制称为 变量对象(variable object)

一个变量对象(缩写形式 - VO)是包含执行上下文的特殊对象,并且保存着:

  • variables( var, 变量声明 )
  • function declarations(函数声明,缩写形式为FD)
  • 函数形参

以上内容均在上下文中声明。

Notice:在ES5中,变量对象的概念已经被词汇环境模型所取代。

举例来说,可以将变量对象表示为普通的ESMAScript对象:

1
VO = {};

正如我们所说,变量对象是执行上下文的一个属性,则:

1
2
3
4
5
activeExecutionContext = {
VO: {
// context data(var, FD, function arguments)
}
}

只有全局上下文中的变量对象可以通过VO的属性名称间接访问、使用(其中全局变量自身就是变量对象)。对于其他的上下文,直接访问VO是不可能的,因为它(VO)纯粹是实现机制(内部的事情)。

当我们声明变量或者函数的时候,除了使用变量名和值创建VO的新属性外,没有其他的事情了。

例如:

1
2
3
4
5
var a = 10;
function test(x) {
var b = 20;
};
test(30);

对应的变量对象是:

1
2
3
4
5
6
7
8
9
10
11
// 全局的变量对象
VO(globalContext) = {
a: 10,
test: <test> fn
}

// 函数 test 中的变量对象
VO(test functionContext) = {
x: 30,
b: 20
}

但是在具体的实现层级(和规范中),变量对象只是抽象的事物(实际上是不存在的)。从根本上来说,在不同的具体执行上下文中,VO的名称和初始结构都是不同的。

不同执行上下文中的变量对象

变量对象的某些操作(例如:变量实例化)和表现对于所有的执行上下文类型都成很普通的。从这个角度出发,将变量对象当作为一个抽象的基础物质更容易理解。函数上下文还可以定义域变量对象相关的其他详细信息。

1
2
3
4
5
6
7
8
AbstractVO (变量实例化过程的一般行为)


╠══> GlobalContextVO
║ (VO === this === global)

╚══> FunctionContextVO
(VO === AO, <arguments> object and <formal parameters> are added)

我们来详细分析一下:

全局上下文中的VO

首先,要给出Global对象的定义:

全局对象是在进入任何执行上下文之前就被创建好的。这个对象只存在一份,他的属性可以在进程的任何地方访问,进程结束,全局对象的声明周期结束。

在创建时候,全局对象通过 MathStringDateparseInt 等属性进行初始化,还可以附加其他对象作为属性,其中也包括引用全局对象自身的对象。例如:在BOM(浏览器对象模型)中,全局对象的 window 属性就是指向全局的(当然,并不是所有的实现都是这样的)。

请看下面的这个例子: windos 是global的属性,但同时值是global。

1
2
3
4
5
6
global = {
Math: <...>,
String: <...>,
...,
window: global
}

当引用全局对象属性的时候,通常是省略前缀的,因为全局对象不可以直接通过名称访问。但是,可以通过全局上下文中的 this 访问,也可以通过递归自己调用自己(例如BOM中的window)来访问。所以,代码可简写为:

1
2
3
4
5
String(10); // 等同于 global.String(10);

// 有前缀
window.a = 10; // === global.window.a = 10 === global.a = 10;
this.b = 20; // global.b = 20;

回到全局上下文中的变量对象,这里的变量对象就是全局变量本身。

1
VO(globalContext) === global;

准确理解 全局上下文中的变量对象就是全局变量自身 是非常有必要的,基于这个事实,在全局上下文中声明一个变量的时候,我们才可以通过全局对象的属性访问到这个变量(例如:实现未知变量名时):

1
2
3
4
5
6
7
8
9
var a = new String('test');

alert(a); // 直接获取到,因为在VO中找到了:'text'

alert(window['a']); // 间接获取到,通过 global === VO: 'text'
alert(a === this.a); // true this === window

var akey = 'a';
alert(window[akey]); // 间接获取到,通过动态属性名 akey === 'a'

函数上下文中的变量对象

关于函数的执行上下文,VO是不能直接获取的。此时由活动对象(activation object)扮演VO的角色。

1
VO(functionContext) === AO;

活动对象在进入函数上下文的时候被创建,并且有一个属性名为 argumants ,属性值为 Argumants Object 的初始值:

1
2
3
AO = {
arguments: <ArgO>
}

Arguments Object 是活动对象的属性,他包含以下属性:

  • callee:指向当前函数的引用
  • length:实际传递的参数的数量
  • properties-indexes(属性索引,字符串类型的整数):属性的值就是函数的参数值(按照参数列表从左往右排列)。属性索引的数量==arguments.length。属性索引对应的值和实际传进来的参数是 共享的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
function foo(x, y, z) {

// 定义的函数的参数数量 (x, y, z)
alert(foo.length); // 3

// 实际传递参数的数量 (only x, y)
alert(arguments.length); // 2

// 函数自身的引用
alert(arguments.callee === foo); // true

// 参数共享

alert(x === arguments[0]); // true
alert(x); // 10

arguments[0] = 20;
alert(x); // 20
11549
x = 30;
alert(arguments[0]); // 30

// 但是,对于未传递参数的z,属性索引的值是不共享的

z = 40;
alert(arguments[2]); // undefined

arguments[2] = 50;
alert(z); // 40

}

foo(10, 20);

关于最后一个例子,在 chrome 的老版本中存在一个bug — 即,没有传递参数z,z 与 arguments[2] 的仍然是共享的。

处理上下文代码的阶段

现在,我们终于进入到本文的关键部分,处理上下文代码的过程被分为两个基本阶段:

  1. 进入执行上下文
  2. 代码运行

变量对象的修改与这两个阶段有着密切的关联。

注意:这两个阶段的处理是一般行为,与上下文类型无关。(对于全局和函数上下文都是公平的)。

进入执行上下文

当进入执行上下文(但是是在代码运行 之前),VO被下面这些属性填充(在前文已经描述过)(从上往下优先级依次降低)

  • 函数的每一个形参(如果我们是在函数执行上下文) — 变量对象的一个属性,这个属性由形参的名称与值组成;如果没有传递实际参数,那么这个属性就由形参形式的名称和 undefined 的值组成。
  • 每一个函数声明(FunctionDeclaration, FD) — 变量对象的一个属性,这个属性的名称是函数名,值是这个函数对象,如果这个变量对象已经拥有了相同名称的属性,那么完全替换这个属性。
  • 每一个变量声明(var, VariableDecalartion) — 变量对象的一个属性,这个属性的名称是变量名,值是 undefined 。如果这个变量名和已经声明的形参或者是函数名称相同,那么这个变量不会影响已经存在的属性。

来看一个例子:

1
2
3
4
5
6
7
function test(a, b) {
var c = 10;
function d() {}
var e = function _e() {}
(function x(){})
}
test(10);

当进入到 test 函数的上下文的时候,test函数接收了一个实参 10,AO对象如下:

1
2
3
4
5
6
7
AO(test) = {
a: 10,
b: undefined,
c: undefined,
d: <FD d>,
e: undefined
}

注意,AO中并没有包含函数 x,这是因为 x 并不是一个函数声明而是一个函数表达式(FunctionExpression, 缩写形式:FE),不影响VO(即这里的AO)。

但是,函数 _e 也是一个函数表达式,就像接下来要看到的,它是被分配给了变量 e,它可以通过变量 e 来访问。关于函数声明( FunctionDeclaration )和函数表达式( FunctionExpression )的不同将会在Chapter 5. Functions中讲到。

代码执行

这个时候,AO/VO已经被各种属性填满了(但是,不是所有的属性都已经由具体的值了,他们中的大部分的初始值都还是 undefined )。

所有代码以及环境不变的情况下,上面的代码中,AO/VO在代码解释器间被修改为如下:

1
2
AO['c'] = 10;
AO['e'] = <FE _e>;

再次注意,因为FE _e 是被保存在变量 e 中,所以,它仍然存在于内存(理解成AO/VO)中。但是FE x 不在了。如果我们在定义之前或者时候调用 x 函数,我们会得到一个错误: x is not defined 。没有保存到一个变量的函数表达式(FE)只能立即执行或者是递归调用。

另一个经典例子:

1
2
3
4
5
6
alert(x); // function
var x = 10;
alert(x); // 10
x = 20;
function x() {};
alert(x); // 20

为什么第一次 alert x 的是一个函数,而且,还是在声明之前?为什么不是 10 或者 20 ?因为,根据规则 — 当进去执行上下文的时候,VO是由函数声明填充的。同时,在相同的阶段,进入执行上下文的时候,有一个 x 的变量声明,但是我们上面已经提到了,如果这个变量名和已经声明的形参或者是函数名称相同,那么这个变量不会影响已经存在的属性。因此,当进入执行上下文的时候。VO进行如下填充:

1
2
3
4
VO = {};
VO['x'] = <FD X>;
// 找到x的变量声明,但是x已经存在了,所以变量声明无效
VO['x'] = <值没有被影响,仍然是 function x>

当到了函数执行阶段,VO进行如下填充:

1
2
VO['x'] = 10;
VO['x'] = 20;

这就是我们在第二次 alert 和第三次 alert 看到的内容。

下面的例子中,我们看到,当进入执行上下文阶段的时候变量都被存放在了 VO 中(虽然 else 语句块没有执行,但是, b 依然存在于 VO中)

1
2
3
4
5
6
7
8
if (true) {
var a = 1;
} else {
var b = 2;
}

alert(a); // 1
alert(b); // undefined 不是 b is not defined.

关于变量

通常很多关于JavaScript的文章或者数据中都指出:“不管是使用var关键字(在全局上下文)还是不使用var关键字(在任何地方),都可以声明一个变量”。 根本就不是这样的请记住:

变量声明只能通过 var 关键字进行声明。

就像这样:

1
a = 10;

这仅仅只是在全局对象上创建了一个新的属性(而不是一个变量)。“不是变量”不是表示不能被修改,而是指ESMAScript规范中的“不是变量”。(由于 VO(globalContext) === global的原因,也会成为全局对象上的属性,还记得吗?)

让我们用代码来展示两者的不同

1
2
3
4
5
alert(a); // undefined
alert(b); // b is not defined

b = 10;
var a = 20;

所有这些都取决于VO及其修改的阶段(进入上下文阶段和代码执行阶段):

进入上下文:

1
2
3
VO = {
a: undefined
};

我们可以看到,这里没有 b ,因为这不是一个变量。 b 只会出现在在代码执行阶段(但是上面的例子中不会出现,因为出错了)。

来改一下这段代码:

1
2
3
4
5
6
7
alert(a); // undefined 我们知道为什么

b = 10;
alert(b); // 10 创建 代码执行阶段

var a = 20;
alert(a); // 20 代码执行阶段修改

关于变量还有一个很重要的观点。与简单属性相反,变量具有 DontDelete 属性(ES5中为 [[Configurable]]),意味着我们不能通过 delete 删除变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
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} 属性:

1
2
3
4
5
6
eval('var a = 10;');
alert(window.a); // 10

alert(delete a); // true

alert(window.a); // undefined

使用一些调试工具(例如:Firebug)的控制台测试该实例时,请注意,Firebug同样是使用eval来执行控制台里你的代码。因此,变量属性同样没有 {DontDelete}特性,可以被删除。

特殊实现:__parent__属性(不重要了)

前面已经提到过,按照标准规范,活动对象是不能直接访问的。然而,一些具体的实现并没有按照这个标准,例如 SpiderMonkey 和Rhino。在这些视实现中,函数具有特殊的属性 __parent__ ,通过这个属性可以访问到已经创建的活动对象(或者是全局变量对象)。

例如(SpiderMonkey, Rhino):

1
2
3
4
5
6
7
var global = this;
var a = 10;
function foo() {};
alert(foo.__parent__); // global
var VO = foo.__parent__;
alert(VO.a); // 10
alert(VO === global);

在上面的例子中我们可以看到,函数foo是在全局上下文中创建的,所以属性parent 指向全局上下文的变量对象,即全局对象。(译者注:还记得这个吧:VO(globalContext) === global)

然而,在SpiderMonkey中用同样的方式访问激活对象是不可能的:在不同版本的SpiderMonkey中,内部函数的parent 有时指向null ,有时指向全局对象。

在Rhino中,用同样的方式访问激活对象是完全可以的。

例如 (Rhino):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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__ of the current activation
// object is already the global object,
// i.e. the special chain of variable objects is formed,
// so-called, a scope chain
print(AO.__parent__ === global); // true

print(AO.__parent__.x); // 10

})();
文章作者: 踏浪
文章链接: https://www.lyt007.cn/技术/ECMA-262-3深入解析第二章:变量对象.html
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 平凡的生活,不平凡的人生
支付宝
微信打赏