深入理解JavaScript系列(12):变量对象(Variable Object)

介绍

JavaScript编程的时候总避免不了声明函数和变量,以成功构建我们的系统,可是解释器是怎样而且在什么地方去查找这些函数和变量呢?我们引用这些对象的时候到底发生了什么?

原始公布:Dmitry A. Soshnikov
公布时间:2009-06-27
俄文地址:http://dmitrysoshnikov.com/ecmascript/ru-chapter-2-variable-object/

英文翻译:Dmitry A. Soshnikov
公布时间:2010-03-15
英文地址:http://dmitrysoshnikov.com/ecmascript/chapter-2-variable-object/

部分难以翻译的句子參考了justinw的中文翻译

大多数ECMAScript程序猿应该都知道变量与运行上下文有密切关系:

var a = 10; // 全局上下文中的变量
 
(function () {
  var b = 20; // function上下文中的局部变量
})();
 
alert(a); // 10
alert(b); // 全局变量 "b" 没有声明

而且。非常多程序猿也都知道。当前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就是运行上下文的属性(property):

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: <reference to function>
};
 
// test函数上下文的变量对象
VO(test functionContext) = {
  x: 30,
  b: 20
};

在详细实现层面(以及规范中)变量对象仅仅是一个抽象概念。

(从本质上说,在详细运行上下文中,VO名称是不一样的,而且初始结构也不一样。

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

对于全部类型的运行上下文来说,变量对象的一些操作(如变量初始化)和行为都是共通的。从这个角度来看。把变量对象作为抽象的基本事物来理解更为easy。相同在函数上下文中也定义和变量对象相关的额外内容。

抽象变量对象VO (变量初始化过程的一般行为)
  ║
  ╠══> 全局上下文变量对象GlobalContextVO
  ║        (VO === this === global)
  ║
  ╚══> 函数上下文变量对象FunctionContextVO
           (VO === AO, 而且加入了<arguments>和<formal parameters>)

我们来具体看一下:

全局上下文中的变量对象

首先,我们要给全局对象一个明白的定义:

全局对象(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"

函数上下文中的变量对象

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

VO(functionContext) === AO;

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

AO = {
  arguments: <ArgO>
};

Arguments对象是活动对象的一个属性,它包含例如以下属性:

  1. callee — 指向当前函数的引用
  2. length — 真正传递的參数个数
  3. 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);

这个样例的代码,在当前版本号的Google Chrome浏览器里有一个bug  — 即使没有传递參数z,z和arguments[2]仍然是共享的。

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

如今我们最终到了本文的核心点了。运行上下文的代码被分成两个主要的阶段来处理:

  1.     进入运行上下文
  2.     运行代码

变量对象的改动变化与这两个阶段紧密相关。

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

进入运行上下文

当进入运行上下文(代码运行之前)时,VO里已经包括了下列属性(前面已经说了):

    函数的全部形參(假设我们是在函数运行上下文中)

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

    全部函数声明(FunctionDeclaration, FD)

    —由名称和相应值(函数对象(function-object))组成一个变量对象的属性被创建。假设变量对象已经存在同样名称的属性,则全然替换这个属性。

    全部变量声明(var, VariableDeclaration)

    — 由名称和相应值(undefined)组成一个变量对象的属性被创建;假设变量名称跟已经声明的形式參数或函数同样。则变量声明不会干扰已经存在的这类属性。

让我们看一个样例:

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

当进入带有參数10的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”来訪问。 函数声明FunctionDeclaration与函数表达式FunctionExpression 的不同。将在第15章Functions进行具体的探讨,也能够參考本系列第2章揭秘命名函数表达式来了解。

这之后,将进入处理上下文代码的第二个阶段 — 运行代码。

代码运行

这个周期内,AO/VO已经拥有了属性(只是,并非全部的属性都有值。大部分属性的值还是系统默认的初始值undefined )。

还是前面那个样例, AO/VO在代码解释期间被改动例如以下:

AO['c'] = 10;
AO['e'] = <reference to FunctionExpression "_e">;

再次注意。由于FunctionExpression“_e”保存到了已声明的变量“e”上。所以它仍然存在于内存中。

而FunctionExpression “x”却不存在于AO/VO中,也就是说假设我们想尝试调用“x”函数,无论在函数定义之前还是之后,都会出现一个错误“x is not defined”,未保存的函数表达式仅仅有在它自己的定义或递归中才干被调用。

还有一个经典样例:

alert(x); // function
 
var x = 10;
alert(x); // 10
 
x = 20;
 
function x() {};
 
alert(x); // 20

为什么第一个alert “x” 的返回值是function,并且它还是在“x” 声明之前訪问的“x” 的?为什么不是10或20呢?由于,依据规范函数声明是在当进入上下文时填入的。 允许周期,在进入上下文的时候另一个变量声明“x”,那么正如我们在上一个阶段所说。变量声明在顺序上跟在函数声明和形式參数声明之后,并且在这个进入上下文阶段。变量声明不会干扰VO中已经存在的同名函数声明或形式參数声明。因此。在进入上下文时,VO的结构例如以下:

VO = {};
 
VO['x'] = <reference to FunctionDeclaration "x">
 
// 找到var x = 10;
// 假设function "x"没有已经声明的话
// 这时候"x"的值应该是undefined
// 可是这个case里变量声明没有影响同名的function的值
 
VO['x'] = <the value is not disturbed, still function>

紧接着。在运行代码阶段,VO做例如以下改动:

VO['x'] = 10;
VO['x'] = 20;

我们能够在第二、三个alert看到这个效果。

在以下的样例里我们能够再次看到。变量是在进入上下文阶段放入VO中的。(由于,尽管else部分代码永远不会运行,可是无论如何,变量“b”仍然存在于VO中。

)

if (true) {
  var a = 1;
} else {
  var b = 2;
}
 
alert(a); // 1
alert(b); // undefined,不是b没有声明,而是b的值是undefined

关于变量

通常。各类文章和JavaScript相关的书籍都声称:“无论是使用varkeyword(在全局上下文)还是不使用varkeyword(在不论什么地方),都能够声明一个变量”。

请记住,这是错误的概念:

不论什么时候,变量仅仅能通过使用varkeyword才干声明。

上面的赋值语句:

a = 10;

这不过给全局对象创建了一个新属性(但它不是变量)。

“不是变量”并非说它不能被改变,而是指它不符合ECMAScript规范中的变量概念,所以它“不是变量”(它之所以能成为全局对象的属性,全然是由于VO(globalContext) === global。大家还记得这个吧?)。

让我们通过以下的实例看看详细的差别吧:

alert(a); // undefined
alert(b); // "b" 没有声明
 
b = 10;
var a = 20;

全部根源仍然是VO和进入上下文阶段和代码运行阶段:

进入上下文阶段:

VO = {
  a: undefined
};

我们能够看到,由于“b”不是一个变量,所以在这个阶段根本就没有“b”,“b”将仅仅在代码运行阶段才会出现(可是在我们这个样例里,还没有到那就已经出错了)。

让我们改变一下样例代码:

alert(a); // undefined, 这个大家都知道,
 
b = 10;
alert(b); // 10, 代码运行阶段创建
 
var a = 20;
alert(a); // 20, 代码运行阶段改动

关于变量。另一个重要的知识点。

变量相对于简单属性来说。变量有一个特性(attribute):{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__ 属性

前面已经提到过,按标准规范,活动对象是不可能被直接訪问到的。

可是。一些详细实现并没有全然遵守这个规定,比如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中用相同的方式訪问活动对象是不可能的:在不同版本号的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
 
})();

总结

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

有不论什么问题,我非常高兴在以下评论中能帮你解答。

原文地址:https://www.cnblogs.com/gccbuaa/p/6946775.html