变量对象

一、概要

在构建系统过程中,我们会在程序中定义一些变量和函数。然而对于解释器来说,它是如何获取这些数据的?当引用一个对象的时候,在解释器内部又发生什么?
许多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
Fork me on GitHub