最近更新
阅读排行
关注本站

JavaScript 变量对象(Variable object)

阅读:3464 次   编辑日期:2013-11-21

目录:

概述:

在我们编写JavaScript的时候,总是需要声明一些变量,或者声明一些函数来构建我们的系统,但是解释器是如何查找这些函数或者变量的呢?我们引用这些对象的时候到底发生了什么呢? 大部分程序猿都知道,变量与执行上下文是有着密切的联系的。
    var a = 10; // 全局上下文中的变量

	(function () {
	  var b = 20; // function上下文中的局部变量
	})();

	alert(a); // 10
	alert(b); // 全局变量 "b" 没有声明(not defined)
并且许多程序员也都知道,ECMAScript标准中指出独立的作用域只有通过“函数(function)代码”才能创建出来。与C/C++不同的是,在ECMAScript中for循环的代码块并不能创建出地上下文:
   for (var k in {a: 1, b: 2}) {
	  alert(k);
	}
    alert(k); // 就算循环结束了,变量对象K仍然在作用域中
让我们来看看,生命数据的时候发生了什么。

数据声明:

如果变量与执行上下文相关,那变量自己应该知道它的数据存储在哪里,并且知道如何访问。这种机制称为变量对象(variable object)。
变量对象(缩写为VO)是一个与执行上下文相关的特殊对象,它存储着在上下文中声明的以下内容:
变量 (var, 变量声明);
函数声明 (FunctionDeclaration, 缩写为FD);
函数的形参
举个例子,可以用ECMAScript的对象来表示变量对象:
   VO = {};

	VO同时也是一个执行上下文一个属性:

	activeExecutionContext = {
	  VO: {
		// 上下文中的数据 (变量声明(var), 函数声明(FD), 函数形参(function arguments))
	  }
	};
对变量的间接引用(通过VO的属性名)只允许发生在全局上下文中的变量对象上(全局对象本身就是变量对象,这部分会在后续作相应的介绍)。
对于其他的上下文而言,是无法直接引用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,并且初始的结构也可能完全不同。
不同执行上下文中的变量对象: 对于所有类型的执行上下文来说,变量对象的一些操作(如变量初始化)和行为都是共通的。从这个角度来看,把变量对象作为抽象的基本事物来理解更为容易。同样在函数上下文中也定义和变量对象相关的额外内容。
抽象变量对象VO (变量初始化过程的一般行为)
  ║
  ╠══> 全局上下文变量对象GlobalContextVO
  ║        (VO === this === global)
  ║
  ╚══> 函数上下文变量对象FunctionContextVO
           (VO === AO,  object and  are added)
接下来对这块内容进行详细介绍。
全局上下文中的变量对象:
首先,有必要对全局对象(Global object)作个定义。
全局对象是一个在进入任何执行上下文前就创建出来的对象;此对象只存在一个;它的属性在程序任何地方都可以直接访问,其生命周期随着程序的结束而终止。
全局对象在初始创建的时候,像 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;
所以,回到全局上下文中的变量对象——在这里,变量对象就是全局对象自己:
    VO(globalContext) === global;
准确地理解这个事实是非常重要:正是由于这个原因,当在全局上下文中声明一个变量时,可以通过全局对象上的属性来间地引用该变量(例如:当变量名提前未知的情况下)。
    var a = new String('test');

    alert(a); // 直接访问,在VO(globalContext)里找到:"test"

    alert(window['a']); // 间接通过global访问:global === VO(globalContext): "test"
    alert(a === this.a); // true

    var aKey = 'a';
    alert(window[aKey]); // 间接通过动态属性名称访问:"test"
ps:关于new String():
当 String() 和运算符 new 一起作为构造函数使用时,它返回一个新创建的 String 对象,存放的是字符串 a 或 a 的字符串表示。所以a相当于一个对象,这个对象=‘test’。
当不用 new 运算符调用 String() 时,它只把 a 转换成原始的字符串,并返回转换后的值。

函数上下中的变量对象:

在函数执行上下文中,VO是不能直接访问的,此时由活跃对象(activation object,缩写为AO)扮演VO的角色。
	VO(functionContext) === AO;
活跃对象会在进入函数上下文的时候创建出来的,初始化的时候会创建一个arguments属性,其值就是Arguments对象:
AO = {
arguments: 
    };
Arguments对象是活跃对象上的属性,它包含了以下几个属性:
callee — 指向当前函数的引用
length — 实参个数
properties-indexes (整数转换成字符串)的值是函数参数的值(按参数列表从左到右排列)。 properties-indexes内部元素的个数等于arguments.length(实参个数). properties-indexes 的值和实际传递进来的参数之间是共享的。
    function foo(x, y, z) {

    // 定义的函数参数(x,y,z)的个数
    alert(foo.length); // 3

    // 实际传递的参数个数
    alert(arguments.length); // 2

    // 引用函数自身
    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,
    // 相关的arguments对象的index-property是不共享的

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

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

    }

    foo(10, 20);
但是这个例子,在Google Chrome浏览器中有个bug——参数z和arguments[2]也是互相共享的,所以在chrome中alert(z)的值为50,而不是40。
现在,就到了本文最核心的部分了,处理中的执行上下文的代码被分成两个基本阶段:
1.进入执行上下文
2.执行代码
对变量对象的修改和这两个阶段密切相关。
要注意的是,这两个处理阶段是通用的行为,与上下文类型无关(不管是全局上下文还是函数上下文都是一致的)。
一旦进入执行上下文(在执行代码之前),VO就会被一些属性填充(在此前已经描述过了):
函数形参(当进入函数执行上下文时) —— 变量对象的一个属性,其属性名就是形参的名字,其值就是实参的值;对于没有传递的参数,其值为undefined。
函数声明(FunctionDeclaration, FD) —— 变量对象的一个属性,其属性名称和值都是函数对象创建出来的;如果之前已经有了相同名称的变量,则函数名成替换函数名称。
变量声明(var,VariableDeclaration) —— 变量对象的一个属性,其属性名称即为变量名,其值为undefined;如果变量名和已经声明的函数名或者函数的参数名相同,则不会替换,说白了,函数名称优先。
看个例子:
    function test(a, b) {
    var c = 10;
    function d() {}
    var e = function _e() {};
    (function x() {});
    }

    test(10); // 调用test
当10为参数进入“test”函数上下文的时候,对应的AO如下所示:
    AO(test) = {
    a: 10,
    b: undefined,
    c: undefined,
    d: 
    e: undefined
    };
注意,AO里并不包含函数“x”。因为“x” 是一个函数表达式(FunctionExpression, 缩写为 FE) 而不是函数声明,函数表达式不会对VO造成影响。 但是,函数“_e” 同样也是函数表达式,但是因为它分配给了变量 “e”,所以它可以通过名称“e”来访问,变量“e”相当于一个对象。
接下来,将进入处理上下文代码的第二个阶段 — 执行代码阶段。 执行代码:
此时,AO/VO的属性已经填充好了。(尽管,大部分属性都还没有赋予真正的值,都只是初始化时候的undefined)。
接着上面的例子来说,到了执行代码阶段,AO/VO就会修改成为如下形式:
    AO['c'] = 10;
    AO['e'] = ;
这里需要注意一下,这里的函数表达式“_e”仍在内存中,这是因为它被保存在声明的变量“e”中,而同样是函数表达式的“x”却不在AO/VO中: 如果尝试在定义前或者定义后调用“x”函数,这时会发生“x为定义”的错误。未保存的函数表达式只有在定义或者递归时才能调用。
下面是很典型的例子:
    alert(x); // function
    var x = 10;
    alert(x); // 10

    x = 20;

    function x() {};

    alert(x); // 20
在上面这个例子中,为何第一个“x”打印出来是函数呢?为何在声明前就可以访问到?又为何不是10或者20呢?原因在于,根据规则——在进入上下文的时候,VO会被填充函数声明; 同一阶段,有函数声明“x”,还有变量声明“x”,但是,此前说过,名称相同的情况下,函数声明优先的原则, 因此,在进入上下文的阶段,VO填充为如下形式:
    VO = {};

    VO['x'] = <引用了函数声明“x”>

    // 发现var x = 10;
    // 如果函数“x”还未定义
    // 则 "x" 为undefined, 但是,在我们的例子中之后又有了函数声明“x”,函数声明优先。

    VO['x'] = <值不受影响,仍是函数>
    之后就到了代码执行阶段,VO被修改成下面这样:
    VO['x'] = 10;
    VO['x'] = 20;
所以第二个,第三个alert的结果回事10,20。
下面这个例子,在进入执行上下文阶段的时候,变量储存在VO中,就算else永远不会执行到,但是b却在VO中,因为代码块是没有本地上下文的。
    if (true) {
    var a = 1;
    } else {
    var b = 2;
    }

    alert(a); // 1
    alert(b); // undefined,不是 not defined,证明存在这个变量。

关于变量:

很多时候,无论是文章,书籍(甚至是面试中)都会说到,声明变量的方式有两种,一种是显式声明使用var关键字,一种是隐式声明 不用var关键字。这种说法是错误的,请记住:使用var关键字是声明变量的唯一方式。
如下赋值语句:
    a = 10;
仅仅是在全局对象上创建了新的属性(而不是变量)。“不是变量”并不意味着它无法改变,它是ECMAScript中变量的概念(它之后可以变为全局对象的属性,因为VO(globalContext) === global,还记得吧?)
但是 刚才说过“使用var关键字是声明变量的唯一方式。”,这两个方式有什么区别呢?
    alert(a); // undefined
    alert(b); // "b" is not defined
    b = 10;
    var a = 20;
接下来还是要谈到VO和在不同阶段对VO的修改(进入上下文阶段和执行代码阶段):
进入上下文阶段:
    VO = {
    a: undefined
    };
我们看到,这个阶段VO并没有被“b”填充,因为它不是变量,“b”在执行代码阶段才出现。(但是,在我们这个例子中也不会出现,因为在“b”出现前就发生了错误 not defined)。
让我们对上面的代码稍作改动:
    alert(a); // undefined, 这个大家都知道
    b = 10;
    alert(b); // 10, 代码执行阶段创建

    var a = 20;
    alert(a); // 20, 代码执行阶段修改
这里还有非常重要的一点需要注意:变量相对与简单的属性来说有一个特点,变量是不能删除的{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); // 仍然为 20,因为变量是不能够删除的。
但是,这里有一个意外情况,就是在“eval”的上下文中,变量是可以删除的:
    eval('var a = 10;');
    alert(window.a); // 10

    alert(delete a); // true

    alert(window.a); // undefined
利用某些debug工具,在终端测试过这些例子的童鞋要注意了:其中Firebug也是使用了eval来执行终端的代码。因此,这个时候var也是可以删除的。
实现层的特性:__parent__属性:
前面说过,活跃对象AO是不能直接访问的,但是在某些实现中并没有完全遵守这个约定,比如知名的SpiderMonkey和Rhino,函数有个特殊的属性__parent__, 该属性是对该函数创建所在的活跃对象的引用(或者全局变量对象)。
如下所示(SpiderMonkey,Rhino):
    var global = this;
    var a = 10;

    function foo() {}

    alert(foo.__parent__); // global

    var VO = foo.__parent__;

    alert(VO.a); // 10
    alert(VO === global); // true
在上述例子中,可以看到,函数“foo”是在全局上下文中创建的,相应的,它的__parent__属性设置为全局上下文的变量对象,比如说:全局对象。
然而,在SpiderMonkey中以相同的方式获取活跃对象是不可能的:不同的版本表现都不同,内部函数的__parent__属性会返回null或者全局对象。
在Rhino中,以相同的方式获取活跃对象是允许的,如下所示(Rhino):
    var global = this;
    var x = 10;

    (function foo() {

    var y = 20;

    // "foo"上下文里的活跃对象
    var AO = (function () {}).__parent__;

    print(AO.y); // 20

    // 当前活动对象的__parent__ 是已经存在的全局对象
    // 变量对象的特殊链形成了
    // 所以我们叫做作用域链
    print(AO.__parent__ === global); // true

    print(AO.__parent__.x); // 10

    })();

结论:

在本篇文章中,我们深入学习了跟执行上下文相关的对象。我希望这些知识对您来说能有所帮助,能解决一些您曾经遇到的问题或困惑。按照计划,在后续的章节中,我们将探讨作用域链,标识符解析,闭包等问题。

关于本文:

本文翻译自 Dmitry Soshnikov 的文章 Variable object.
将本篇文章分享到:
top