一、概要
在构建系统过程中,我们会在程序中定义一些变量和函数。然而对于解释器来说,它是如何获取这些数据的?当引用一个对象的时候,在解释器内部又发生什么?
许多ECMAScript程序都知道,变量和执行上下文是密切相关的:
var a = 10;//全局上下文中的变量
(function(){
var b = 20;//函数上下文中的局部变量
})();
alert(a);// 10
alert(b);// b is not defined
不仅如此,许多程序员也都知道,ECMAScript标准中指出独立的作用域只有通过”函数代码”才能创建。与C/C++不同的是,ECMAScript中的for循环代码块是无法创建本地上下文的:
for(var k in {a:1,b:2}){
alert(k);
}
alert(k);//尽管循环已经结束,但是变量"k"仍然在作用域中
二、数据声明
既然变量和执行上下文有关,那么它就该知道数据存储在哪里以及如何获取。这种机制就称作变量对象:
变量对象是一个特殊的对象,它与执行上下文息息相关。变量对象主要存储变量、函数声明、函数入参等
举个例子,可以用ECMAScript的对象来表示变量对象: VO = {};
VO同时也是一个执行上下文的属性:
activeExecutionContext = {
VO : {
//上下文中的数据(变量声明、函数声明、函数形参)
}
}
声明新的变量和函数的过程其实就是在VO中创建新的变量以及函数名对应的属性和属性值的过程。
如下所示:
var a = 10;
function test(x){
var b = 20;
}
test(30);
上述代码对应的变量对象:
//全局上下文中的变量对象
VO(globalContext) = {
a : 10,
test :
}
//test函数上下文中的变量对象
VO(test functionContext) = {
x : 30,
b : 20
};
但是,在实现层,变量对象只是一个抽象的概念。在实际执行上下文中,VO可能完全不叫VO,并且初始的结构也可能完全不同。
三、全局上下文的变量对象
全局对象是一个在进入任何执行上下文前就创建出来的对象,此对象以单例形式存在;它的属性在程序任何地方都可以访问,其声明周期随着程序的结束而终止。
全局对象在创建的时候,诸如Math、String、Date、parseInt等等属性也会被初始化,同时,其中一些对象会指向全局对象本身。比如,DOM中,全局对象上的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;
var a = new String('test');
alert(a);//直接访问,通过VO(globalContext)访问
alert(window['a']);//间接访问
alert(a === this.a);//true
四、函数上下文中的变量对象
在函数的执行上下文中,VO是不能直接访问的。它主要扮演被称作活跃对象(activation object)的角色。
VO(functionContext) === AO;
活跃对象会在进入函数上下文的时候创建出来,初始化的时候会创建一个argument属性,其值就是Argument对象:
Argument对象是活跃对象上的属性,它包含了如下属性:
- callee—对当前函数的引用
- length—实参的个数
argument对象的properties-index的值与当前(实际传递的)形参是共享的
function foo(x,y,z){ //定义的函数的参数个数 alert(foo.length);//3 //实际传递参数的个数 alert(argument.length);//2 //引用函数自身 alert(argument.callee === foo);//true //参数共享 alert(x === argument[0]);true alert(x);//10 argument[0] = 20; alert(x);// 20 x = 30; alert(argument[0]);//30 //然而对于没有传递的参数z //相关的argument对象的properties-index是不共享的 z = 40; alert(argument[2]);//undefined argument[2] = 50; alert(z);//40 }
处理上下文代码的几个阶段:
1、进入执行上下文
一旦进入执行上下文(在执行代码之前),VO就会被一些属性填充:
- 函数的形参—-变量对象的属性,其属性名就是形参的名字,其值就是实参的值,对于没有传递的参数,其值为undefined
- 函数声明—-变量对象的属性,其属性名和值都是函数对象创建出来的,如果变量对象已经包含了相同名字的属性,则进行替换
变量声明—-变量对象的一个属性,其属性名为变量名,其值为undefined,如果变量名和已声明的函数名或者函数参数名相同,则不会影响已存在的属性。
function test(a,b){ var c = 10; function d(){} var e = function _e(){}; (function x(){}); } test(10);//函数调用
当以10为参数进入test函数上下文的时候,对应的AO如下所示:
A0(test) = { a : 10, b : undefined, c : undefined, e : undefined, d : <reference to FunctionDeclaration "d"> }
上面的AO中并不包含函数x,这是因为这里的x并不是函数声明,而是函数表达式,函数表达式不会对VO造成影响。
2、执行代码
此时,AO/VO的属性已经填充好了。(尽管大部分的属性都还没有赋予真正的值,都只是初始化时候的undefined值)
继续以上面的例子为例,到了执行代码阶段,AO/VO就会修改成如下形式:
AO['c'] = 10;
AO['e'] = <reference to FunctionExpress>
以下是更加典型的例子:
alert(x);//function
var x = 10;
alert(x);// 10
x = 20;
function x(){}
alert(x);// 20
正如此前提到的,变量声明是在函数声明和函数形参之后,并且变量声明不会对已存在的同名的函数声明和函数形参发生冲突,因此,在进入上下文阶段,VO填充如下形式:
VO = {};
VO['x'] = <reference to FunctionDeclaration "x">
//发现var x = 10;
//如果函数"x"还未定义
//但是在我们的例子中变量声明并不会影响同名的函数值
VO['x'] = <值不受影响,仍是函数引用>
关于变量
大多数讲Javascript的文章甚至Javascript的书通常都会这么说:声明全局变量的方式有两种,一种是使用var关键字,另外一种是不用var关键字,而这样的描述是错误。要记住的是:使用var关键字是声明变量的唯一方式。
如下赋值语句:a = 10;仅仅只是在全局对象上创建了新的属性(而不是变量)。
不同点如下:
alert(a);//undefined
alert(b);//"b" is not defined
b = 10;
var a = 20;
接下来要谈到VO和不同阶段对VO的修改(进入上下文阶段和代码执行阶段):
进入上下文:
VO = {
a : undefined
};
我们看到,这个阶段并没有任何”b”,因为它不是变量,b在执行阶段出现。
将上述代码稍作改动:
alert(a);//undefined
b = 10;
alert(b);//10,代码执行阶段建立
var a = 20;
alert(a);//20,代码执行阶段赋值
关于变量还有非常重要的一点是:与简单属性不同的是,变量是不能删除的,这意味着想要通过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);//仍然是20
但是,这里有一个例外,就是在eval执行上下文中,是可以删除变量的。
eval('var a = 10');
alert(window.a); // 10
alert(delete a); // true
alert(window.a); // undefined