本文译自Dmitry Soshnikov的《ECMA-262-3 in detail》系列教程。其中会加入一些个人见解以及配图举例等等,来帮助读者更好的理解JavaScript。
声明:本文不涉及与ES6相关的知识。
前言
在学习变量对象之前,我们要对执行期上下文有所了解,可以先看过《从ECMAScript规范深度分析JavaScript(一):执行期上下文》一文再来进行对变量对象的学习。
在程序中,我们会声明一些函数和变量来帮助我们成功构建系统,但是解释器是如何并且在哪里找到我们的数据(函数,变量)的呢?当我们引用所需的对象时,发生了什么呢?
许多ECMAScript程序员都知道变量对象和执行期上下文(execution context)密切相关:
var a = 10; // 全局上下文(global context)的全局变量
(function () {
var b = 20; // 函数上下文function context对的局部变量
})();
alert(a); // 10
alert(b); // "b" is not defined
同时,很多程序员知道,在当前版本规范中独立作用域只能由函数代码类型的执行期上下文创建。比如,以ECMAScript中的for循环块为例,不同于C/C++,它不会创建一个局部上下文(即局部作用域):
for (var k in {a: 1, b: 2}) {
alert(k);
}
alert(k); //当循环结束后,k变量依然存在于作用域中。
让我们一起来深入了解一下当我们声明我们的数据时,到底发生了什么事情。
数据声明
如果变量和执行期上下文相关,那么他就应该知道他的数据在哪儿存储以及如何在哪儿获取。这个机制叫做变量对象(variable object)。
一个变量对象(variable object,简称VO)是一个与上下文相关的特殊对象(注:这个对象在js不可获取,只是一个实现上的概念),存储了在上下文中声明的以下内容:
- 变量声明
- 函数声明
- 函数的形参
在ES5中变量对象(VO)的概念被词法环境(lexical environment)模型所替代了,理解了变量对象(VO)的概念,后面的新概念也很容易就理解了。
简单举个例子,我们完全可以将变量对象表示为一个普通ECMAScript对象:
VO = {};
如我们所说,变量对象VO是一个执行期上下文的一个属性:
activeExecutionContext = {
VO: {
// 上下文数据 (变量,函数,形参)
}
};
间接的引用变量(通过VO的属性名)只在全局上下文中被允许(因为在全局上下文里,全局对象自身就是变量对象)。在其它上下文中是不可能直接访问到VO的,因为变量对象仅仅是实现机制。
当我们声明了一个变量或者函数,除了使用它们的名称和值在VO中创了一个新属性外,其他什么事也没做。
比如:
var a = 10;
function test(x) {
var b = 20;
};
test(30);
对应的变量对象为:
// 全局上下文的变量对象
VO(globalContext) = {
a: 10,
test: <reference to function>
};
// test函数上下文的变量对象
VO(test functionContext) = {
x: 30,
b: 20
};
但是我们要注意:在实现层面和规范上,变量对象只是一个抽象的事物。从本质上讲,在具体的执行期上下文中,VO的名称不同并且有不同的初始结构。
不同执行期上下文中的变量对象
在所有类型的执行期上下文中,变量对象的一些操作(比如变量声明)和行为都是一致的。从这个观点很容易将变量对象作为一个抽象的基本事物来描述。函数上下文可以对变量对象定义一些额外的细节
AbstractVO (变量初始化的通用行为)
║
╠══> GlobalContextVO
║ (VO === this === global)
║
╚══> FunctionContextVO
(VO === AO, 包含额外的arguments对象和形参)
我们来对此进行深入讨论
1、全局上下文中的变量对象
在这里,首先有必要给出全局对象(Global object)的概念:
全局对象是在进入执行期上下文之前进行创建,只存在一个全局对象,全局对象的属性在程序的任何地方都能够访问到,全局对象的声明周期在程序结束时才结束。
在全局对象创建的时候,会初始化一些比如Math,String,Date,parseInt等属性,同样也有一些额外指向全局对象自身的属性——比如在BOM中,全局对象的window属性指向全局对象(注意,不是在所有实现中都是这样):
global = {
Math: <...>,
String: <...>
...
...
window: global
};
因为全局对象没法通过名称来直接访问,所以当引用全局对象的属性时通常会省略前缀。但是,我们可以通过全局对象的this值来访问,也可以通过引用自身的属性来访问,比如BOM中的window,所以我们简写为:
String(10); // global.String(10);
// 有前缀
window.a = 10; // === global.window.a = 10 === global.a = 10;
this.b = 20; // global.b = 20;
所以,说回到全局上下文的变量对象,就是全局对象本身:
VO(globalContext) === global;
正确理解这个概念是很有必要的,因为由于这个原因,在全局上下文中声明一个变量,我们可以通过全局对象的属性名间接的引用它(在我们没法提前知道这个属性名的时候这将会很有用):
var a = new String('test');
alert(a); // 直接引用, 会在VO(globalContext)中找到
alert(window['a']); // 通过 global === VO(globalContext间接引用
alert(a === this.a); // true
var aKey = 'a';
alert(window[aKey]); // 当属性名为动态不确定时,间接引用
2、函数上下文的变量对象
我们知道,在函数执行上下文中,VO是不能直接访问的,此时由激活对象(activation object,简称为AO)扮演VO的角色。
VO(functionContext) === AO;
激活对象在进入函数上下文时被创建,并且由Arguments对象作为arguments属性进行初始化:
AO = {
arguments: <ArgO>
};
Argunemts对象时激活对象的一个属性,包含以下属性:
- callee:当前函数的引用;
- length:实际传参的数量;
- properties-indexes(integer会被转为string)属性下标:该属性的值就是函数的参数值(按参数列表从左到右排列),内部元素个数等于arguments.length,arguments对象的properties-indexes值与实际形参参数是共享的(因为引用地址相同)。
例如:
function foo(x, y, z) {
// 函数定义形参的个数 (x, y, z)
alert(foo.length); // 3
// 实际传参的数量 (只有 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
x = 30;
alert(arguments[0]); // 30
// 但是,没有传递参数z,相关的参数对象的下标属性不共享
z = 40;
alert(arguments[2]); // undefined
arguments[2] = 50;
alert(z); // 40
}
foo(10, 20);
最后一个例子中的场景,在某些浏览器中有bug,就是即使没有传递参数z,z和arguments[2]仍然是共享的(其实也不能说是bug,因为以前的处理机制中它们俩的引用地址就是同一个,所以改变arguments[2]的值,z当然也会跟着改变)。
注:在ES5中激活对象的概念也被公共的和单一模式的词法环境的概念所取代。
结语
本文讨论了数据声明是变量对象的变化,以及在不同执行期上下文中变量对象的区别和在某些浏览器中需要注意的点。
在下一篇文章《从ECMAScript规范深度分析JavaScript(三):变量对象(下)》中我们将进一步讲解变量对象的概念和知识。
希望此文能够解决大家工作和学习中的一些疑问,避免不必要的时间浪费,有不严谨的地方,也请大家批评指正,共同进步!
转载请注明出处,谢谢!
交流方式:QQ1670765991