变量对象
Variable Object,简称 VO,它是一个与执行上下文相关的特殊对象。它存储着在上下文中定义的:
变量声明
函数声明 (FunctionDeclaration,FD)
函数的形参
我们可以把 VO 理解为执行上下文的一个属性(property)。当我们声明一个变量或一个函数的时候,就像我们为 VO 增加一个新属性一样。
变量对象只是一个抽象概念。(从本质上说,在具体执行上下文中,VO 名称是不一样的,并且初始结构也不一样),对于所有类型的执行上下文来说,变量对象的一些操作(如变量初始化)和行为都是共通的。
抽象变量对象 VO (变量初始化过程的一般行为)
║
╠══> 全局上下文变量对象 GlobalContextVO
║ (VO === this === global)
║
╠══> 函数上下文变量对象 FunctionContextVO
║ (VO === AO, 并且添加了 arguments 和 formal parameters)
无论有多少个函数上下文,但是全局上下文只有一个。
全局上下文VO
全局上下文的 VO 其实是我们非常熟悉的一个对象,全局对象(Global Object):
- 在进入任何执行上下文之前就已经创建了的对象
- 只存在一份,它的属性在程序中任何地方都可以访问,生命周期终止于程序退出那一刻
如:在客户端中 Js 中,全局对象就是 Window
注:只有全局上下文的 VO 允许通过 VO属性名称 来间接访问(因为在全局上下文里,全局对象自身就是变量对象),在其它上下文中是不能直接访问 VO 对象的,因为它只是内部机制的一个实现。
函数上下文VO/AO
只有到当进入一个执行上下文中,这个执行上下文的变量对象才会被激活,而只有被激活的变量对象,也就是活动对象上的各种属性才能被访问。
进入函数执行上下文之前
- VO 中的属性是不能直接访问的。
进入函数执行上下文时
- VO 转变成了活动对象(Activation Object,AO),里面的属性都能被访问了,也就是说 AO 是在进入到执行上下文的时候被激活
- AO 通过函数的 arguments 属性初始化(arguments 属性的值是 Arguments 对象)。
处理上下文代码过程
执行上下文的代码被分成两个基本的阶段来处理:
1.创建阶段(进入执行上下文)
2.执行代码
变量对象的修改变化与这两个阶段紧密相关。
注:这2个阶段的处理是一般行为,和上下文的类型无关(即在全局上下文和函数上下文中的表现是一样的)。
进入执行上下文
当进入执行上下文时(此时代码还没执行),VO 会按如下顺序进行初始化:
1.函数的所有形参(如果是在函数执行上下文中)
- 由名称和对应值组成的一个变量对象的属性被创建
- 没有传递对应参数的,属性值为 undefined
2.函数声明(FunctionDeclaration,FD)
- 由名称和对应值(函数对象(function-object))组成一个变量对象的属性被创建
- 如果变量对象已经存在相同名称的属性,则完全替换这个属性
3.变量声明(VariableDeclaration,VD)
- 由名称和对应值(undefined)组成一个变量对象的属性被创建
- 若变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性
例:
function test(a, b) {
var c = 10;
function d() {}
var e = function _e() {};
(function x() {});
}
test(10); // call
进入函数 EC 时:
AO(test) = {
arguments: {
0: 10,
1: undefined,
length: 2
},
a: 10,
b: undefined,
c: undefined,
d: <reference to FunctionDeclaration "d">
e: undefined
};
注:AO 里并不包含函数 x 。这是因为 x 是一个函数表达式(FunctionExpression, FE)而不是函数声明,函数表达式不会影响变量对象。
执行代码
在执行代码阶段,AO 已经拥有了属性(不过,并不是所有的属性都有值,大部分属性的值还是系统默认的初始值 undefined)。
此时会顺序执行代码,根据代码,修改变量对象的值。接上边例子,此时 AO:
AO(test) = {
arguments: {
0: 10,
1: undefined,
length: 2
},
a: 10,
b: undefined,
c: 10,
d: <reference to FunctionDeclaration "d">
e: <reference to FunctionDeclaration "_e">
};
注:通常,当执行期上下文被销毁时,函数的 AO 也就被销毁了。但是当有闭包引用时,激活对象就不会被销毁,因为他仍然被引用。
别急,闭包我们会在后边的章节细说。
示例
console.log(foo);
function foo(){
console.log("foo");
}
var foo = 1;
结果:打印函数。
分析:之前我们是用变量提升来解释的,现在我们可以更深一层,用变量对象来解释了,由 VO 的初始化顺序得知:若变量名跟已声明的函数相同,则变量声明不会干扰它。
也就是说,初始化会先处理函数声明,其次才处理变量声明
关于变量(拓展)
例一:
a = 10;
分析:
这仅仅是给全局对象创建了一个新属性,然而它并不是变量,因为它不符合 ECMAScript 规范中的变量概念。它之所以能成为全局对象的属性,完全是因为 VO(globalContext) === global
例二:
alert(a); // undefined
alert(b); // Uncaught ReferenceError: b is not defined
b = 10;
var a = 20;
分析:
根源仍然是 VO、进入上下文阶段、代码执行阶段。
在进入上下文阶段:
VO = {
a: undefined
};
我们可以看到,因为 b 不是一个变量,所以在这个阶段根本就没有 b,b 将在代码执行阶段才会出现(但是在我们这个例子里,还没有到那就已经出错了)。
例三:
alert(a); // undefined, 这个大家都知道
b = 10;
alert(b); // 10, 代码执行阶段创建
var a = 20;
alert(a); // 20, 代码执行阶段修改