Javascript基础系列之变量对象
前言
本文翻译自variable-object
概要
在程序中我们免不了要声明函数和变量去创建应用程序。然后,解析器是怎样以及去哪里找到这些数据(函数、变量)?当我们引用一个变量时,在解析器内部又发生了什么?
带着这样的问题,看下面代码
var a = 10; // variable of the global context
(function () {
var b = 20; // local variable of the function context
})();
alert(a); // 10
alert(b); // "b" is not defined
同样的,大家都知道基于当前版本,独立作用域只能通过函数代码才能创建。它和C/C++
不同,在ECMAScript中for循环不会创建一个局部上下文
数据(函数/变量)声明
如果变量与执行上下文相关,那么它就应该知道数据储存在哪里以及如何访问这些数据,这种机制被称为变量对象(variable object)
变量对象(variable object, VO)是一个与某个执行上下文相关的特殊对象,并且存储了以下数据
- 变量
- 函数声明
- 函数形参
notice: 在 ES5 中,变量对象和活动对象并入了词法环境模型(lexical environments model),详细的描述请看这里。
简单的举个例子,可以使用ECMAScript 的对象来表示变量对象
VO = {}
正如我们所有,VO是执行上下文的一个属性
activeExecutionContext = {
VO: {
// context data (var, FD, function arguments)
}
};
只有全局执行上下文中的变量对象才可以通过VO访问(因为全局对象就是VO对象本身),在其他上下文中是不能直接访问VO对象的,因为它只是内部机制的一个实现(抽象的)
当我们声明一个变量或者函数时,等于在VO对象上添加了一个对应的属性键值对
例如:
var a = 10;
function test(x) {
var b = 20;
};
test(30);
对应的变量对象如下:
VO(globalContext) = {
a:10,
test:<reference to function>
}
VO(test functionContext) = {
x:30,
b:20
}
但是,在实现层面上(和规范中)变量对象只是一个抽象概念。从本质上说,在实际执行上下文中,VO 可能完全不叫 VO,而且其初始结构也可能完全不同
不同执行上下文中的变量对象
对所有类型执行上下文,变量对象的一些操作(如变量对象)和行为都是相同的。从这个角度来看,把变量对象表示为抽象对象概念更多合适。而在函数执行上下文中也可以给变量对象定义相关的额外细节
AbstractVO (变量实例化过程中的通用行为)
║
╠══> GlobalContextVO
║ (VO === this === global)
║
╚══> FunctionContextVO
(VO === AO, <arguments> object and <formal parameters> are added)
全局执行上下文中的变量对象
在这,非常有必要给变量对象一个定义
全局对象是一个在进入任何执行上下文之前就创建的对象,此对象以单例的形式存在,它的属性在程序任何地方都可以访问,其生命周期随着程序的结束而终止。
全局对象建立时候,像Math
、String
、Date
、parseInt
等属性会被初始化,同样也能添加其他对象作为属性,其中包括可以引用全局对象自身的属性。比如,在BOM中,window
属性就是引用全局对象自身
global = {
Math: <...>,
String: <...>
...
...
window: global
};
在引用全局对象属性时候,前缀通常是可以省略的,因为全局对象是不能通过名字直接访问的。然而,我们还是可以通过this直接访问全局对象,也可以通过全局对象属性来访问到全局对象,例如,DOM中的window属性
String(10); // 等同于 global.String(10);
// 带前缀
window.a = 10; // === global.window.a = 10 === global.a = 10;
this.b = 20; // global.b = 20;
因此,全局执行上下文中的变量对象就是全局对象自身
VO(globalContext) == VO
函数上下文中的变量对象
函数执行上下文中,变量对象(VO)是不可以直接访问的,此时活动对象(AO)扮演着VO的角色
VO(fucntionContext) == AO
当进入函数执行上下文的时候,活动对象被创建,同时伴随着 arguments 属性的初始化,该属性是 Arguments 对象的值:
AO = {
arguments:
};
arguments 对象是活动对象(AO)中的一个属性,包含以下属性
- callee -- 当前函数引用
- length - 实参数量
- properties-indexes(字符串类型的整数),属性的值就是函数的参数值(按参数列表从左到右排列)。properties-indexes 的元素的个数等于 arguments.length,properties-indexes 的值和实际传递进来的参数之间是共享的
例如:
function foo(x, y, z) {
// 形参个数
alert(foo.length); // 3
// 实参个数
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 = 40;
alert(arguments[2]); // undefined
arguments[2] = 50;
alert(z); // 40
}
foo(10, 20);
处理上下文代码的几个阶段
到此,将是本文最核心的部分了。处理执行上下文分为两个部分
- 进入执行上下文
- 执行代码
变量对象的修改和两个阶段息息相关。这两个处理阶段是通用的行为,与上下文类型无关(不管是全局上下文还是函数上下文都是一致的)。
进入执行上下文
当进入执行上下文时(在代码执行前),VO就会被下列属性填充(在此前已经描述过了)
- 函数所有的参数(如果是在函数执行上下文中)都对应变量对象中的一个属性。该属性由形参名和对应的实参值构成,如果没有传递实参,那么该属性值就为 undefined
- 所有函数声明(FunctionDeclaration, FD) 每个函数声明都对应变量对象中的一个属性,这个属性由一个函数对象的名称和值构成,如果变量对象中存在相同的属性名,则完全替换该属性。
- 所有变量声明都对应变量对象中的一个属性,该属性的键/值是变量名和 undefined,如果变量名与已经声明的形参或函数相同,则变量声明不会干扰已经存在的这类属性。
如下:
function test(a, b) {
var c = 10;
function d() {}
var e = function _e() {};
(function x() {});
}
test(10); // call
当进入test的执行上下文,并传递了实参10,AO对象如下:
AO(test) = {
a: 10,
b: undefined,
c: undefined,
d: <reference to FunctionDeclaration "d">
e: undefined
};
注意AO中并不包含X函数,因为X函数不是函数声明,而是一个函数表达式(FE),函数表达式不会影响AO
执行代码
此时,AO/VO属性已经填充完毕(尽管很多属性值还是undefined)
继续上个例子,到了执行阶段,AO/VO就会修改为如下形式
AO['c'] = 10;
AO['e'] = <reference to FunctionExpression "_e">;
再次注意,函数表达式 _e
仍在内存中,它被保存在声明的变量 e 中。但函数表达式 x 却不在 AO/VO 中,如果尝试在其定义前或者定义后调用 x 函数,这时会发生“x未定义”的错误。未保存在变量中的函数表达式只能在其内部或通过递归才能被调用
另外一经典例子:
alert(x); // function
var x = 10;
alert(x); // 10
x = 20;
function x() {};
alert(x); // 20
为什么第一次弹出的是 “function”?为何在 x 声明前就能访问到?为什么弹出的不是 “10” 或者 “20”?原因在于,根据规范,在进入上下文时,VO 中的 x 被填充为函数声明。同时,还有变量声明 x,但是,根据前面的规则,变量声明是在函数形参和函数声明之后,并且,变量声明不会影响已经存在的同名函数或形参,因此,进入上下文时,VO 如下:
VO = {};
VO['x'] = <引用了函数声明“x”>
// 发现var x = 10;
// 如果函数“x”还未定义
// 则 "x" 为 undefined, 但是,在我们的例子中
// 变量声明并不会影响同名的函数值
VO['x'] = <值不受影响,仍是函数>
随后在代码执行阶段
VO['x'] = 10;
VO['x'] = 20;
下一篇: js打开新窗口