javascript系列之变量对象
引言 |
一般在编程的时候,我们会定义函数和变量来成功的构造我们的系统。但是解析器该如何找到这些数据(函数,变量)呢?当我们引用需要的对象时,又发生了什么了?
很多ECMAScript编程人员都知道变量和所处的执行上下文环境是密切相关的:
1 var a=10;//全局上下文环境下的变量 2 (function(){ 3 var b=20;//函数上下文环境下的局部变量 4 })(); 5 alert(a);//10 6 alert(b);//"b" 未定义
当然,许多编程人员也知道。在当前规范版本下,隔离的作用域只能由“function”代码的执行上下文产生。与c/c++不同的是,例如ECMAScript中的for循环语句块不能产生局部的执行上下文:
1 for(var k in {a:1,b:2}){ 2 alert(k); 3 } 4 alert(k);//即使循环结束,变量'k'任然在作用域中
下面让我们看看,当我们声明我们的数据时发生的更多的细节。
数据声明 |
如果变量和执行上下文是密切联系的,就应该知道数据存储在哪里,如何获取这些数据。这种机制就称为变量对象。
变量对象(VO)是一个与执行上下文和其存储位置密切联系的特殊对象:
- 变量(var ,变量声明);
- 函数声明(FD);
- 函数形参;
在上下文中被声明。注意,在EC5中用词法环境模式取代了变量对象。
理论上,可以把变量对象表示为一个常规的ECMAScript对象:VO={};正如我们所说,VO是执行上下文的一个属性:
1 activeExecutionContext={ 2 Vo:{ 3 //上下文数据(var,FD,function arguments) 4 } 5 };
一般不能直接引用变量。仅仅能(通过VO的属性名)引用全局上下文的变量对象(全局对象就是他自身的变量对象)。至于其他的执行上下文直接引用VO是不可能的,它仅仅是一种实现层面的纯粹机制。
当我们声明一个变量或者函数时。我们除了构造VO的包含变量名称和变量值的属性,再没有其他东西了。比如:
var a=10; function test(x){ var b=20;}; test(30);
相应的变量对象是:
1 //全局环境下的变量对象 2 VO(globalContext)={ 3 a=10, 4 test:<reerence to function> 5 }; 6 //"test"函数上下文的变量对象 7 VO(test functionContext)={ 8 x:30, 9 b:20 10 };
但是在执行阶段(标准下),变量对象是一个抽象的本质。在具体的执行上下文中,VO的命名方式不同且有不同的初始结构。
不同执行上下文中的变量对象 |
变量对象的一些操作(比如变量赋值)和行为在所有的执行上下文类型中都是相同的。从这一个角度看,把变量对象表示为一个抽象的基本概念是很方便的。函数上下文也可以定义一些与变量对象相关的附加信息。
1 AbstratVO(变量对象实例化的一般过程) 2 ║ 3 ╠══> GlobalContextVO 4 ║ (VO === this === global) 5 ║ 6 ╚══> FunctionContextVO 7 (VO === AO, <arguments> object and <formal parameters> are added)
下面让我们详细的来讨论下。
全局上下文变量对象 |
在这里,首先应该给出全局对象的定义:全局对象是在进入任何执行上下文前就已经构造出的一个对象;全局对象是唯一的(译者注:单例模式),在程序中的任何地方都可以获取它的属性,其生命周期随着程序的结束而结束。
构造的全局对象被诸如Math,String,Date,parseInt等属性初始化。也可以通过一些可以引用全局对象自身的附加对象初始化。例如,在BOM中,全局对象的的window属性指向全局对象(然而,不是所有的实现都是这样的)
1 global={ 2 Math:<...>, 3 String:<...>, 4 .... 5 .... 6 window:global 7 };
当引用全局对象属性时,前缀通常是被省略的,因为全局对象不能直接通过名称获取。可能要通过全局上下文中的this值来获取,也可以通过递归引用它自身获取,例如BOM中的window,可以简写为:
1 String(10);//表示global.String(10) ; 2 //有前缀 3 window.a=10;//===global.window.a=10===global.a=10; 4 this.b=20;//global.b=20
因此,回到全局上下文中的变量对象—这里的变量对象就是全局对象自身:VO(globalContex)===global;
鉴于这些原因必须准确理解这个事实:在全局上下文声明的一个变量,我们可以通过全局对象的属性间接引用它(例如变量名是未知的)
1 var a=new String('test') 2 alert(a);//直接引用,在VO(globalCOntext):"test" 3 alert(window['a']);//间接引用===VO(globalContext):"test" 4 alert(a===this.a);//true 5 var akey='a'; 6 alert(window[akey]);//间接引用,通过动态属性名:"test"
函数上下文的变量对象 |
对于函数执行上下文—VO是不能直接获取的,它的角色由活动对象(AO)扮演。VO(functionContext)===AO;当进入到一个函数上下文时,就产生了活动对象。并由值为Arguments对象的arguments属性初始化。
1 AO={arguments:<Arguments Object> }
Arguments对象是活动对象的属性。它包含了以下属性:
- callee--函数自身的引用;
- length--实参个数;
- properties- indexes(整数,转化为字符),其值是函数参数的值(参数列表从左至右)。properties- indexes==arguments.length.也就是参数对象的properties-indexes值和当前(实际传入值)的形参是共享的
1 function foo(x, y, z) { 3 // 已定义的函数参数 (x, y, z)个数 4 alert(foo.length); // 3 6 // 实际传参数量(only x, y) 7 alert(arguments.length); // 2 9 // 函数自身的引用 10 alert(arguments.callee === foo); // true 12 // 参数共享 14 alert(x === arguments[0]); // true 15 alert(x); // 10 17 arguments[0] = 20; 18 alert(x); // 20 20 x = 30; 21 alert(arguments[0]); // 30 23 // 然而对于未传参的z,arguments参数对象的索引属性时不共享的 27 z = 40; 28 alert(arguments[2]); // undefined 30 arguments[2] = 50; 31 alert(z); // 40 33 }
在低版本的google浏览器中参数共享存在漏洞。在EC5中。活动对象的概念已经被词法环境的公有和单例模式所取代。
处理上下文代码的阶段 |
现在我们进入到文章的重点,处理执行上下文代码分为两个阶段:
- 进入执行上下文;
- 执行代码。
变量对象的修正与这两个阶段也是密切相关的。需要注意的是,这两个阶段的处理过程是一般性的行为并独立于上下文类型(也就是说,这个过程对于两种执行上下文-函数和全局都是平等的)
进入执行上下文 |
在进入执行上下文时(在代码执行执行前),VO已经被以下属性(他们已经在前文中提到)填充。
- 对于函数的每一个形参(如果我们已经进入了函数执行上下文)--- 一个含有名称和形参值的变量对象属性就创建了,参数还未传值--也就是含有形参名和其值为undefined的属性被创建。
- 对于每一个函数声明(FD)--- 一个含有函数对象名称和值的属性就创建了;如果变量对象已经包含了同名的属性,覆盖他的值和特性;
- 对于每一个变量声明--- 一个含有变量名和其值为undefined的属性就创建了;如果这个变量名和已经声明的形参或函数名称一样,变量声明不能与已经存在的属性冲突(译者注:此变量名称不可用,换之)。
让我们看下面的例子;
1 function test(a,b){ 2 var c=10; 3 function d(){}; 4 var e=function _e(){}; 5 (function x(){}); 6 } 7 test(10)
当进入含有实参10的test函数上下文时,AO如下:
1 AO(test) = { 2 a: 10, 3 b: undefined, 4 c: undefined, 5 d: <reference to FunctionDeclaration "d"> 6 e: undefinedhttp://i.cnblogs.com/EditPosts.aspx?postid=3711963 7 };
注意,这个AO不包含函数X,这是因为X不是一个函数声明而是函数表达式(FE),表达式不影响VO。然而函数_e也是一个函数表达式,但我们将在VO里 面找到,这是因为把它赋值给变量e了,它是通过e来获取的。函数声明和函数表达式在后面会详细讨论。这些结束后就进入了处理上下文代码的第二个阶段--代 码执行阶段。
代码执行 |
在这个时候,AO/VO已经包含了这些属性(虽然不是所有的属性都有了我们传递的真实值,但大部分已经有了初始的值undefined).同样的例子,在代码解析时AO/VO做如下的修正:
1 AO['c'] = 10;
2 AO['e'] = <reference to FunctionExpression "_e">;
我们还要注意的这个函数表达式_e仅仅只存在于内存中,因为保存在在已声明的变量e里。但是函数表达式x没有在AO/VO中,如果我们在定义之前或定义之 后调用x函数,我们将会得到错误:"x" is not defined.未保存的函数表达式仅能在它定义的地方调用或者递归的调用。
一个经典实例:
1 alert(x)//function x(){}
2 var x=10;
3 alert(x);//10
4 x=20;
5 function x(){}
6 alert(x);//20
为什么一开始弹出X是一个函数,且在声明之前就能过获取了?为什么不是10或者20?因为,根据规则—在进入上下文之前VO是被函数声明填充的。与此同 时,这里有一个变量声明x,但我们上面已经提到,语义化的变量声明阶段在函数声明和形参声明之后。在这期间变量还不能和已经声明的函数和形参名称冲突。因 此,在进入VO上下文时:
1 VO={}; 2 VO['x']=<reference to FunctionDeclaration "x"> 3 //var x=10; 4 //if function "x"还没定义,"x"为未定义。但是在这种情况下,变量声明不能干扰同名的函数。 5 VO['x']=<值没有被破坏,任然是function>
在代码执行阶段,VO修正如下
1 VO['x']=10;
2 VO['x']=20;
我们在第二和第三个alert出的结果。
在下面的例子中在进入上下文阶段我们再次看到变量放入了VO中(因此,else从不被执行,但尽管如此,变量b还是存在VO中):
1 if(true){ 2 var a=1; 3 }else{ 4 var b=1; 5 } 6 alert(a);//1 7 alert(b);//undefined but not "b is not defined"
关于变量 |
许多关于javascript的文章甚至是书本说道:"使用var关键字(在全局执行环境)和不使用var关键字(在任何地方)声明全局变量是可能的"。其实不是这样的。请记住:变量只能通过var关键字声明。
像这样赋值:a=10;仅仅创建了全局对象的新属性(而不是变量)。在这种意义下“Not the variable”并不是不能被改变的,但是在ECMAScript的变量概念下(由于VO(globalContext)===global,我们记住 了嘛?),它成为了全局对象的属性。
不同之处在下面(通过例子来展示)
1 alert(a);//undefined 2 alert(b);//b is not defined 3 b=10; 4 var a=20;
所有的都依赖于VO和他的修正阶段(进入执行上下文和代码执行阶段):
进入上下文:
1 VO = {a: undefined};
我们看到在这个阶段这里没出现任何b,因为他不是变量。b仅仅在代码执行阶段出现(在这种情况下是不会有错的)。我们修改代码如下:
1 alert(a); // undefined, we know why 3 b = 10; 4 alert(b); // 10, created at code execution 6 var a = 20; 7 alert(a); // 20, modified at code execution
关于变量这里有更重要的一点。变量和简单的属性不同,有{DontDelete}
属性,意味着不能通过delete操作符删除一个变量:
1 a=10; 2 alert(window.a);//10 3 alert(delete a);//true 4 alert(window.a);//undefined 5 var b=20; 6 alert(window.b);//20 7 alert(delete b);//false 8 alert(window.b);//still 20
记住:在ES5中{DontDelete}重命名为[[Configureable]],并能通过Object.defineProperty方法手工管 理。然而有一种执行上下文中这个规则是不起作用的。他就是EVAL上下文:变量不再设置{DontDelete}属性:
1 eval('var a = 10;'); 2 alert(window.a); // 10 4 alert(delete a); // true 6 alert(window.a); // undefined
对那些在控制台来验证这些例子的调试工具来说,例如firebug:记住,firebug也是在控制台使用eval来执行你的代码。所以这些变量也没有{DontDelete}
属性,并且可以被删除的。
实现层的特征:_parent_属性 |
我们已经注意到,在标准情况下。直接获取活动对象时不可能的。然而,在一些实现中,诸如SpiderMonkey 和 Rhino。函数有一个特殊的属性_parent_
。他可以引用已经在函数中产生的活动对象。
例子 (SpiderMonkey, Rhino):
1 var global=this; 2 var a=10; 3 function foo(){} 4 alert(foo._parent_);//global 5 var VO=foo._parent_; 6 alert(VO.a);//10 7 alert(VO===global);//true
以上的例子中我们看到函数foo()在全局上下文中构造,据此,他的_parent_属性设置为了全局上下文的变量对象也就是全局对象。然而在SpiderMonkey用同一种方式获取活动对象是不可能的:依据不同的版本,内部函数的
_parent_返回null或者全局对象。
在Rhino中,允许通过同样的方式获取活动对象:
1 var global=this; 2 var a=10; 3 (function foo(){ 4 var y=20; 5 //"foo"函数上下文的活动对象 6 var AO=(function(){})._parent_; 7 alert(AO.y);//20 8 //当前活动对象的_parent_已经变成了全局对象。这样变量对象的一个特殊的链就形成了,就是所谓的作用域链 9 alert(AO._parent_===global);//true 10 alert(AO._parent_.x);//10 11 })()
总结 |
在这篇文章中我们继续深入的与执行上下文有关的对象。我希望这些材料是有用的而且讲清楚了某些你以前觉得的方面。以后的计划,在下面的章节中将会讲到作用域链,确定标示符,最终是闭包。