首页 > 代码库 > javascript系列之变量对象

javascript系列之变量对象

原文: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)是一个与执行上下文和其存储位置密切联系的特殊对象:

  1. 变量(var ,变量声明);
  2. 函数声明(FD);
  3. 函数形参;

在上下文中被声明。注意,在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中。活动对象的概念已经被词法环境的公有和单例模式所取代。

处理上下文代码的阶段

     现在我们进入到文章的重点,处理执行上下文代码分为两个阶段:

  1. 进入执行上下文;
  2. 执行代码。

     变量对象的修正与这两个阶段也是密切相关的。需要注意的是,这两个阶段的处理过程是一般性的行为并独立于上下文类型(也就是说,这个过程对于两种执行上下文-函数和全局都是平等的)

进入执行上下文

    在进入执行上下文时(在代码执行执行前),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 })()
总结

      在这篇文章中我们继续深入的与执行上下文有关的对象。我希望这些材料是有用的而且讲清楚了某些你以前觉得的方面。以后的计划,在下面的章节中将会讲到作用域链,确定标示符,最终是闭包。