首页 > js, 前端开发 > 翻译:ECMA-262-3 深入解析.第二章.变量对象

翻译:ECMA-262-3 深入解析.第二章.变量对象

声明:本文的原作者对本文作了修改,本文中对“Feature of implementations: property __parent__”的翻译来自Justin的blog,新版本的翻译请参考Justin的blog:http://www.cnblogs.com/justinw/archive/2010/04/23/1718733.html

  1. 导言
  2. 数据声明
  3. 不同执行上下文中的变量对象
    1. 全局上下文中的变量对象
    2. 函数上下文中的变量对象
  4. 上下文代码处理分期
    1. 进入执行上下文
    2. 代码执行
  5. 关于变量
  6. 工具特征:property __parent__
  7. 结论
  8. 其它参考

导言

在程序中我们总要声明变量和函数,然后成功的用它们来构建我们的系统。当我们引用需要的对象时,解释器如何、在哪里找到我们的数据(functions,variable),会发生什么?

很多ECMAScript 程序员都清楚变量与执行上下文密切相关。

var a = 10; // variable of the global context
(function () {
  var b = 20; // local variable of the function context
})();
alert(a); // 10
alert(b); // "b" is not defined

同时,很多程序员也知道,在当前版本的规范中,独立的作用域只能通过“function”代码类型的执行上下文来创建。也就是说,对比C/C++,ECMAScript 中的循环块不能创建一个局部的上下文。

for (var k in {a: 1, b: 2}) {
  alert(k);
}
alert(k); // variable "k" still in scope even the loop is finished

当声明数据时会发生什么?让我们看看更多细节。

数据声明

当变量与执行上下文相关,它应该知道数据存储在哪里,如何得到它。这种机制称之为变量对象。

变量对象(缩写为VO)是与执行上下文相关的对象,它存储:

  • 变量(var VariableDeclaration);
  • 函数声明(FunctionDeclaration 缩写为FD);
  • 以及函数的形参。

在上下文中声明

例如,它可以表现为正常的ECMAScript对象的变量对象:

VO = {};

正如我们所说,VO是执行上下文的属性:

activeExecutionContext = {
  VO: {
    // context data (var, FD, function arguments)
  }
};

间接引用变量(通过VO的属性名)只限于全局上下文的变量对象(在那里全局对象自身就是变量对象,稍后将涉及到)。对于其它作用域,直接引用一个VO是不可能的,它完全是执行机制。

当我们声明一个变量或一个函数时,除此之外,还用我们变量的名字和值为VO创建一个新的属性。

例如:

var a = 10;
function test(x) {
  var b = 20;
};
test(30);

相应的变量对象是:

// Variable object of the global context
VO(globalContext) = {
  a: 10,
  test:<reference to function>
};

// Variable object of the "test" function context
VO(test functionContext) = {
  x: 30,
  b: 20
};

但是,在执行期间(和规范中),变量对象是一个抽象的实体。从根本来讲,在具体的执行上下文中,VO命名不同,并且有不同的初始结构。

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

当一个变量对象是由实体的子元素构成,并且在不同的执行上下文中是“inherited”的关系时,将变量对象表现为基础、抽象的元素是非常容易的。

全局上下文中的变量对象

在这里,首先有必要给全局对象一个定义:

全局对象是一个在进入任何执行上下文之前创建的对象,该对象存在于单拷贝中,它的属性在程序中的任何位置都可访问,其生命周期结束于程序完成时。

在创建时,全局对象通过一些属性如Math, String, Date, parseInt等初始化。也可以附加其它对象,其中可以引用全局对象本身。例如,在DOM中,全局对象的window属性指向全局对象(但不是在所有的执行中)。

global = {
  Math: <...>,
  String: <...>
  ...
  ...
  window: global
};

当引用全局对象的属性时,前缀通常省略。因为全局对象不能直接通过名字访问。但是,为了访问它,通过全局对象的this是可能的,也可以通过递归引用自身。例如,DOM中的window,因此可以简写为:

String(10); // means global.String(10);
// with prefixes
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); // directly, is found in VO(globalContext): "test"
alert(window['a']); // indirectly via global === VO(globalContext): "test"
alert(a === this.a); // true
var aKey = 'a';
alert(window[aKey]); // indirectly, with dynamic property name: "test"

函数上下文中的变量对象

关于函数执行上下文--VO是不能直接访问的,它扮演的角色称之为激活对象(缩写为AO)。

VO(functionContext) === AO;

激活对象在进入一个函数的执行上下文中创建,通过属性参数初始化,其值为参数对象(Arguments )。

AO = {
  arguments: <ArgO>
};

Arguments 对象是函数执行上下文中激活对象中的一个,包含下列属性:

  • callee --当前函数的引用;
  • length --真正传递的参数个数;
  • properties-indexes --(整数,转换成字符串),其值是函数参数的值。这些properties-indexes 的个数 == arguments.length。Arguments对象的properties-indexes值和呈现的形参(真正传递的)共享。

例如:

function foo(x, y, z) {
  alert(arguments.length); // 2 – quantity of passed arguments
  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
  // however, for not passed argument z,
  // related index-property of the arguments
  // object is not shared
  z = 40;
  alert(arguments[2]); // undefined
  arguments[2] = 50;
  alert(z); // 40
 }
foo(10, 20);

在最后一个例子中,Google Chrome的当前版本有一个bug,参数z和arguments[2]是共享的。

上下文代码处理分期

现在,我们已经进入到文章的主要部分。 代码的执行上下文处理分为两个基本阶段:

  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); // call

进入到“test”函数上下文中,传递的参数是10,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 的详细分歧在Chapter 5. Functions 中讨论。

之后,进入上下文代码处理的第二个阶段--代码执行阶段。

代码执行

此时,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 ”。未保存的FunctionExpression只在它的定义中或递归中调用。

还有一个(经典)的例子:

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

为什么第一个alert(x)是“function”,而且在声明之前可以访问。为什么不是10或20?因为根据规则--在进入上下文时,VO填充函数声明。在同一个阶段,进入上下文时有一个变量“x”。但正如我们上面提到的步骤所说,变量声明语义上跟在函数声明和参数声明之后,在这个阶段,它不会干扰已经声明的同一名称的函数或形参,因此,在进入上下文中,VO作如下填充:

VO = {};
VO['x'] = <treference to FunctionDeclaration "x" >
// found var x = 10;
// if function "x" would not be already defined
// then "x" be undefined, but in our case
// variable declaration does not disturb
// the value of the function with the same name
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, but not "b is not defined"

关于变量

通常各类文章,甚至是关于JavaScript的书籍都声称:用var关键字(在全局上下文中)和不用var关键字(在任何地方)声明一个变量是可能的。请记住,并非如此。

变量只使用var关键字来声明。

像这样分配:

a = 10;

仅仅创建全局对象新的属性(而不是变量)。从意义上将,“Not the variable”不是说它不能被改变,而是指ECMAScript变量概念中的“Not the variable”(它成为全局对象的属性,因为VO(globalContext) === global,我们记得,是吧?)

不同之处如下(让我们在例子中展示):

alert(a); // undefined
alert(b); // "b" is not defined
b = 10;
var a = 20;

再次取决于VO和它修改的阶段(进入上下文阶段和代码执行阶段)。

进入上下文:

VO = {
  a: undefined
};

在这个阶段,我们可以看到没有变量“b”,因为它不是一个变量。“b”仅出现在代码执行阶段(既然是一个错误,在我们的例子中就不会出现)。

让我们改变代码:

alert(a); // undefined, we know why
b = 10;
alert(b); // 10, created at code execution
var a = 20;
alert(a); // 20, modified at code execution

关于变量有最重要的一点。与简单的属性相比,变量有一个属性{DontDelete} ,即它不可能通过delete运算符删除属性。(关于delete,在我翻译的《理解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来执行你在控制台中的代码。因此,var 不具有{DontDelete} 属性,可以被删除。

特殊实现:property __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 中用同样的方式获得激活对象是不可能的:取决于不同的版本,内部函数的__parent__ 的要么返回null,要么返回全局对象。

在Rhino中,可以获取激活对象,可用同样的方式。

例如(Rhino):

var global = this;
var x = 10;
(function foo() {
  var y = 20;
  // the activation object of the "foo" context
  var AO = (function () {}).__parent__;
  print(AO.y); // 20
  // __parent__ of the current activation
  // object is already the global object,
  // i.e. the special chain of variable objects is formed,
  // so-called, a scope chain
  print(AO.__parent__ === global); // true

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

})();

结论

在这篇文章中,我么进一步推进与执行上下文相关对象的研究。我希望它是有用的,能澄清一些你以前有过的问题或模糊之处。按照计划,接下来的章节将讨论Scope chain,Identifier resolution ,Closures 。

如果你有问题,我会很乐意的在评论中回答它们。

其它参考

转载地址:http://www.denisdeng.com/?p=878

原文地址: ECMA-262-3 in detail. Chapter 2. Variable object.

第一章参考地址:ECMA-262-3 深入解析.第一章.执行上下文

原文最新更新请及时参考原文

  1. 2010年4月20日22:53 | #1

    发一点勘误:
    1.原文:there is anything else as creation of the new property of VO with name and value of our variable.
    原来翻译:并没有为VO创建一个新的属性,该属性拥有我们变量的名字和值。
    我的理解:主要是通过变量的名称和值创建了VO的新特性(property)

    2.《理解delete》链接错了

    3.特变的属性__parent__
    特别

    4.业已指出,
    前文已经指出

  2. 2010年4月21日00:23 | #2

    已经修正。非常感谢你为我指出这些瑕疵。

  3. 2010年4月24日00:14 | #3

    最新版本请参考原文,原文中:“Variable object in different execution contexts” 中的第一段做了如下修改:
    “Some operations (e.g. variable instantiation) and behavior of the variable object are common for all execution context types. From this viewpoint it is convenient to present variable object as abstract base thing. Function context can also define additional details related with the variable object.”
    最新版本译文请参考Justin的blog:http://www.cnblogs.com/justinw/archive/2010/04/23/1718733.html

  1. 2010年4月18日21:37 | #1
  2. 2010年4月18日21:38 | #2
  3. 2010年4月18日22:10 | #3