引言 |
一般在编程的时候,我们会定义函数和变量来成功的构造我们的系统。但是解析器该如何找到这些数据(函数,变量)呢?当我们引用需要的对象时,又发生了什么了?
很多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 })()
总结 |
在这篇文章中我们继续深入的与执行上下文有关的对象。我希望这些材料是有用的而且讲清楚了某些你以前觉得模棱两可的方面。以后的计划,在下面的章节中将会讲到作用域链,确定标示符,最终是闭包。