深入JavaScript变量对象


作者:Seiya

时间:2019年06月06日


前言


JavaScript 编程的时候总避免不了声明函数和变量,以成功构建我们的系统,但是在 JavaScript 代码执行的时候,解释器是如何查找和使用这些函数和变量呢?我们引用这些对象的时候究竟发生了什么?

上一篇文章《深入JavaScript执行上下文》已经介绍过,JavaScript 变量与执行上下文有密切关系。并且,当前ECMAScript规范也指出独立作用域只能通过“函数(function)”代码类型的执行上下文创建。也就是说,相对于C/C++来说,ECMAScript里的for循环并不能创建一个局部的上下文。



不同执行上下文中的变量对象


对于所有类型的执行上下文来说,变量对象的一些操作(如变量初始化)和行为都是共通的。从这个角度来看,把变量对象作为抽象的基本事物来理解更为容易。


声明

变量对象(缩写为VO)是一个与执行上下文相关的特殊对象,它存储着在上下文中声明的以下内容:

  • 变量声明

  • 函数声明

  • 函数的形参

注意:

只有全局上下文的变量对象允许通过 VO 的属性名称来间接访问(因为在全局上下文里,全局对象自身就是变量对象),在其它上下文中是不能直接访问 VO 对象的,因为它只是内部机制的一个实现。



全局上下文中的变量对象


  • 全局对象

    全局对象(Global object)是在进入任何执行上下文之前就已经创建了的对象;这个对象只存在一份,它的属性在程序中任何地方都可以访问,全局对象的生命周期终止于程序退出那一刻。


    全局对象初始创建阶段将 Math、String、Date、parseInt 等属性作为自身属性进行初始化,同样也可以有额外创建的其它对象作为属性(其可以指向到全局对象自身)。例如,在 DOM 中,全局对象的 window 属性就可以引用全局对象自身:

    global = {
      Math: <...>,
      String: <...>
      ...
      ...
      window: global    // 引用自身
    };
    

  • 访问全局对象

    当访问全局对象的属性时通常会忽略掉前缀,这是因为全局对象是不能通过名称直接访问的。 不过我们依然可以通过全局上下文的this来访问全局对象,同样也可以递归引用自身:

    // 忽略前缀
    String(10);     // 就是global.String(10);
    
    // 带有前缀
    window.a = 10;  // === global.window.a = 10 === global.a = 10;
    this.b = 20;    // global.b = 20;
    


函数上下文中的变量对象


在函数执行上下文中,VO 是不能直接访问的,此时由活动对象(activation object,缩写为AO)扮演 VO 的角色。

活动对象是在进入函数上下文时刻被创建的,它通过函数的arguments属性初始化。arguments属性的值是Arguments对象:

AO = {
  arguments: <Arguments>
};

Arguments 对象包括如下属性:

  • callee:指向当前函数的引用

  • length:真正传递的参数个数

  • properties-indexes (字符串类型的整数) 属性的值就是函数的参数值(按参数列表从左到右排列)

    properties-indexes 内部元素的个数等于 arguments.length. properties-indexes 的值和实际传递进来的参数之间是共享的。


举例如下:

function foo(x, y, z) {
  // 声明的函数参数数量arguments (x, y, z)
  alert(foo.length); // 3

  // 真正传进来的参数个数(only x, y)
  alert(arguments.length); // 2

  // 参数的callee是函数自身
  alert(arguments.callee === foo); // true

  // 参数共享
  alert(x === arguments[0]); // true
  alert(x); // 10
  arguments[0] = 20;
  alert(x); // 20
  x = 30;
  alert(arguments[0]); // 30

  // 不过,没有传进来的参数z,和参数的第3个索引值是不共享的
  z = 40;
  alert(arguments[2]); // undefined

  arguments[2] = 50;
  alert(z); // 40
}

foo(10, 20);


处理上下文代码的两个阶段


执行上下文的代码被分成两个基本的阶段来处理,变量对象的修改变化与这两个阶段紧密相关:

  • 初始化上下文阶段

  • 代码执行阶段

注:

这2个阶段的处理是一般行为,和上下文的类型无关(也就是说,在全局上下文和函数上下文中的表现是一样的)。


初始化上下文阶段

在进入代码执行阶段之前,VO 里已经包含了下列属性:

  • 函数的所有形参(如果我们是在函数执行上下文中)

    • 由名称和对应值组成的一个变量对象的属性被创建;

    • 没有传递对应参数的话,那么由名称和 undefined 值组成的一种变量对象的属性也将被创建;


  • 所有变量声明(var, VariableDeclaration)

    • 由名称和对应值(函数对象)组成一个变量对象的属性被创建;

    • 如果变量对象已经存在相同名称的属性,则完全替换这个属性;


  • 所有函数声明(FunctionDeclaration, FD)

    • 由名称和对应值(undefined)组成一个变量对象的属性被创建;

    • 如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性;


下面来看一个例子:

function test(a, b) {
  var c = 10;
  function d() {}
  var e = function _e() {};
  (function x() {});
}

test(10);     // 调用 test

当进入 test 函数上下文时,AO 表现为如下:

AO(test) = {
  a: 10,
  b: undefined,
  c: undefined,
  d: <reference to FunctionDeclaration "d">
  e: undefined
};

AO 里并不包含函数“x”。这是因为“x” 是一个函数表达式(FunctionExpression, 缩写为 FE) 而不是函数声明,函数表达式不会影响VO。虽然函数“_e” 同样也是函数表达式,但因为它分配给了变量 “e”,所以它可以通过名称“e”来访问。

注意:

FE(函数表达式)“_e”保存到了已声明的变量“e”上,所以它仍然存在于内存中。而 FE “x”却不存在于AO/VO中,也就是说如果我们想尝试调用“x”函数,不管在函数定义之前还是之后,都会出现一个错误“x is not defined”,未保存的函数表达式只有在它自己的定义或递归中才能被调用



代码执行阶段


在进入代码执行阶段之前,AO/VO 已经拥有了属性(不过,并不是所有的属性都有值,大部分属性的值还是系统默认的初始值 undefined )。在代码执行阶段,JavaScript 代码会被顺序执行,根据代码,修改变量对象的值。

还是拿之前的例子解释,进入执行阶段后,JavaScript 引擎会依照顺序修改变量对象的值,最终 AO 的表现如下:

AO(test) = {
  a: 10,
  b: undefined,
  c: 10,
  d: <reference to FunctionDeclaration "d">
  e: function _e() {};
};


关于变量


通常,各类文章和JavaScript相关的书籍都声称:“不管是使用var关键字(在全局上下文)还是不使用var关键字(在任何地方),都可以声明一个变量”。这其实是错误的概念:

任何时候,变量只能通过使用var关键字才能声明。


举个例子:

a = 10;

上面的赋值语句仅仅是给全局对象创建了一个新属性(但它不是变量)。“不是变量”并不是说它不能被改变,而是指它不符合ECMAScript规范中的变量概念,所以它“不是变量”(它之所以能成为全局对象的属性,完全是因为VO(globalContext) === global)


举个例子具体说明:

alert(a);   // undefined
alert(b);   // "b" 没有声明

b = 10;
var a = 20;

因为“b”不是一个变量,所以在这个阶段根本就没有“b”,“b”将只在代码执行阶段才会出现。


关于变量,还有一个重要的知识点。变量相对于简单属性来说,变量有一个特性:{DontDelete},简单来说就是不能用 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);  // still 20

但是这个规则在有个上下文里不起作用,那就是 eval 上下文,变量没有{DontDelete}特性。

举例如下:

eval('var a = 10;');
alert(window.a);    // 10
alert(delete a);    // true
alert(window.a);    // undefined

扩展

使用一些调试工具(例如:Firebug)的控制台测试该实例时,请注意,Firebug同样是使用eval来执行控制台里你的代码。因此,变量属性同样没有{DontDelete}特性,可以被删除。



特殊实现: __parent__ 属性


前面已经提到过,按标准规范,活动对象是不可能被直接访问到的。但是,函数有一个特殊的属性 __parent__,通过这个属性可以直接引用到活动对象(或全局变量对象),如下所示:

var global = this;
var a = 10;

function foo() {}

alert(foo.__parent__); // global

var VO = foo.__parent__;

alert(VO.a); // 10
alert(VO === global); // true
最后更新时间: 2019-7-7 21:55:38