Variable Object in JavaScript
[JavaScript] 变量对象 Variable Object
介绍
本文中,我们将分析与ECMAScript执行上下文相关的概念变量对象 Variable Object
先举个栗子🌰,为什么a b x表现大不相同,当引用一个函数或者变量时,解释器是如何以及从哪里找到它们的呢
alert(a); // undefined
alert(b); // "b" is not defined
b = 10;
var a = 20;
alert(x); // function x() {}
var x = 10;
alert(x); // 10
x = 20;
function x() {};
alert(x); // 20
定义
然变量和执行上下文有关,那它应该知道数据存放在哪以及如何获取,这种机制称为变量对象
变量对象(缩写VO)是与执行上下文相关的特殊对象,它存储在上下文中声明的:
- 变量(
var
,VariableDeclaration)- 函数声明(FunctionDeclaration,简写为FD)
- 函数形式参数
示意地示例,可以用ECMAScript的对象来表示变量对象:
VO = {};
变量对象同时也是执行上下文EC的一个属性:
activeExecutionContext = {
VO: {
// context data (var, FD, function arguments)
}
};
对变量的间接引用(通过VO的属性名)只允许发生在全局上下文中的变量对象上(全局对象本身就是变量对象), 对于其他的上下文,是无法直接引用VO的,因为VO是实现层机制
声明新的变量或函数的过程其实就是用变量或函数的名称和值在VO中创建新的属性的过程:
var a = 10;
function test(x) {
var b = 20;
};
test(30);
上面👆代码对应的变量对象如下:
// Variable object of the global context
VO(globalContext) = {
a: 10,
test: <reference to function>
};
// Variable object of the "test" function context
VO(test functionContext) = {
x: 30,
b: 20
};
但是,但是,在实现层(标准中定义的),变量对象只是一个抽象概念
不同执行上下文中的变量对象
变量对象上的一些操作(比如:变量的初始化)和行为对于所有的执行上下文类型来说都一样,从这点来说,将变量对象表示成抽象的概念更加便捷
AbstractVO (generic behavior of the variable instantiation process)
║
╠══> GlobalContextVO
║ (VO === this === global)
║
╚══> FunctionContextVO
(VO === AO, <arguments> object and <formal parameters> are added)
全局上下文中的变量对象
首先对全局对象(Global object)作个定义
全局对象是一个在进入任何执行上下文前就创建出来的对象,此对象以单例形式存在,它的属性在程序的任何地方都可以直接访问,它的生命周期随着程序的结束而终止
全局对象在创建时,Math,String,Date,parseInt等属性也会被初始化,同时,其中一些对象会指向全局对象本身,如BOM中,全局对象上的window属性就指向全局对象
global = {
Math: <...>,
String: <...>
...
...
window: global
};
在引用全局对象的属性时,前缀通常可以省略,因为全局对象是不能通过名字直接访问的,然而,通过全局对象上的this值,以及通过BOM中的window对象这样递归引用的方式都可以访问到全局对象
String(10); // means global.String(10);
// with prefixes
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); // directly, is found in VO(globalContext): "test"
alert(window['a']); // indirectly via global === VO(globalContext): "test"
alert(a === this.a); // true
var aKey = 'a';
alert(window[aKey]); // indirectly, with dynamic property name: "test"
函数上下文中的变量对象
在函数的执行上下文中,VO是不能直接访问的,它主要扮演活跃对象(activation object)(简称AO)的角色
VO(functionContext) === AO;
活动对象在进入函数上下文时创建,并通过属性arguments进行初始化,其值就是Arguments对象
AO = {
arguments: <ArgO>
};
Arguments对象是活动对象的属性,它包含的属性如下:
- callee 对当前函数的引用
- length 实际参数的数量
- properties-indexes(整数,转换为字符串)其值是函数参数的值(在参数列表中从左到右),properties-indexes的个数 == arguments.length,arguments对象的properties-indexes的值和当前(真正传递的)形式参数是共享的
function foo(x, y, z) {
// quantity of defined function arguments (x, y, z)
alert(foo.length); // 3
// quantity of really passed arguments (only x, y)
alert(arguments.length); // 2
// reference of a function to itself
alert(arguments.callee === foo); // true
// parameters sharing
alert(x === arguments[0]); // true
alert(x); // 10
arguments[0] = 20;
alert(x); // 20
x = 30;
alert(arguments[0]); // 30
// however, for not passed argument z,
// related index-property of the arguments
// object is not shared
z = 40;
alert(arguments[2]); // undefined
arguments[2] = 50;
alert(z); // 40
}
foo(10, 20);
处理上下文代码的阶段
处理执行上下文代码分为两个阶段:
- 进入执行上下文
- 执行代码
对变量对象的修改和这两个阶段密切相关
要注意的是,这两个处理阶段的行为是通用的,与上下文类型无关(不管是全局上下文还是函数上下文)
进入执行上下文
一旦进入执行上下文(但是在执行代码之前),VO就会被填充如下的一些属性:
- 函数的形参(当进入函数执行上下文时)
变量对象的属性,其属性名就是形参的名字,其值就是实参的值,对于没有传递的参数,其值为undefined - 函数声明(FunctionDeclaration,FD)
变量对象的属性,其属性名和值就是函数对象的名称和值,如果变量对象已经包含具有相同名称的属性,则替换它的值 - 变量声明(var,VariableDeclaration)
变量对象的属性,其属性名即为变量名,其值为undefined,如果变量名和已经声明的函数名或函数参数名相同,则不会影响已经存在的属性
看个栗子🌰:
function test(a, b) {
var c = 10;
function d() {}
var e = function _e() {};
(function x() {});
}
test(10); // call
当以10为参数进入test函数上下文时,对应的AO如下:
AO(test) = {
a: 10,
b: undefined,
c: undefined,
d: <reference to FunctionDeclaration "d">
e: undefined
};
注意,上面的AO并不包含函数x
,因为这里的x
不是函数声明而是函数表达式(FunctionExpression,简称FE),函数表达式不会影响VO, 尽管函数_e
也是函数表达式,然而,由于它被赋值给变量e
,因此它可以通过e
来访问到
至此,处理上下文代码的第一阶段介绍完了,接下来介绍第二阶段:执行代码阶段
执行代码
此时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); // function
var x = 10;
alert(x); // 10
x = 20;
function x() {}
alert(x); // 20
上例中,为何x
打印出来是函数呢,为何在声明前就可以访问到,为何10或者20不是这样呢,原因在于,根据规则,在进入上下文时,VO会被函数声明填充,同时还有变量声明x
,但是,变量声明是在函数声明和函数形参之后,并且变量声明不会与已经存在的同名的函数声明和函数形参冲突, 因此在进入上下文的阶段,VO填充为如下形式:
VO = {};
VO['x'] = <reference to FunctionDeclaration "x">
// found var x = 10;
// if function "x" would not be already defined
// then "x" be undefined, but in our case
// variable declaration does not disturb
// the value of the function with the same name
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, but not "b is not defined"
关于变量
一些JavaScript文章甚至是JavaScript书籍经常会说:声明全局变量的方式有两种,一种是使用var关键字(在全局上下文中),另外一种是不用var关键字(在任何位置)
,这样的描述是错误的,记住:
使用var关键字是声明变量的唯一方式
像下面👇的赋值语句:
a = 10;
仅仅是在全局对象上创建了新的属性(而不是变量),不是变量
并不意味着它无法改变,而是按照ECMAScript中变量的概念不是变量
不同之处如下:
alert(a); // undefined
alert(b); // "b" is not defined
b = 10;
var a = 20;
所有这些都取决于VO和其修改的阶段(进入上下文阶段和代码执行阶段)
进入上下文阶段:
VO = {
a: undefined
};
这个阶段并没有任何b
,因为它不是变量,b
在执行代码阶段才出现
将上述代码改动一下:
alert(a); // undefined, we know why
b = 10;
alert(b); // 10, created at code execution
var a = 20;
alert(a); // 20, modified at code execution
关于变量还有一点非常重要:与简单属性不同,变量是不能删除的{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
注意,在ES5中{DontDelete}
被重命名为[[Configurable]]
,并且可以通过Object.defineProperty
方法手动管理
然而,有一个执行上下文不会被这个规则影响到,这就是eval上下文
,其中没有为变量设置{DontDelete}
属性
eval('var a = 10;');
alert(window.a); // 10
alert(delete a); // true
alert(window.a); // undefined