首页
Javascript
Html
Css
Node.js
Electron
移动开发
小程序
工具类
服务端
浏览器相关
前端收藏
其他
关于
公司注册

ECMA-262-3 in detail——第二章:变量对象

2015年03月24日 发布 阅读(2140) 作者:Jerman

原文: JavaScript. The Core: 2nd Edition

介绍 | Introduction

在程序中我们总是声明变量和函数然后用它们来搭建我们的系统。但是解释器(interpreter)是在哪里和以什么方式来找到我们的数据(函数,变量)的呢?

当我们引用所需要的对象时发生了什么?

许多ECMAScript程序员知道变量是和执行上下文(execution context)紧密关联的:

  1. var a = 10; //全局上下文中的变量
  2. (function () {
  3. var b = 20; // 函数上下文中的局部变量
  4. }) ();
  5. alert(a); // 10
  6. alert(b); // "undefined"

同样,许多程序员也知道,在这个版本的规范中,独立作用域(isolated scope)只能通过函数代码类型(“function” code type)的执行上下文创建。换句话说,和C/C++不同,类似for循环的块在ECMAScript中不生成局部上下文:

  1. for (var i = 0; i < 3; i++) {
  2. }
  3. alert(i); // 3

让我们来看下当我们声明一个数据时所发生的过程的更多细节。

数据声明 | Data declaration

如果变量是和执行上下文相关的,那么它应当知道它的数据储存在哪里和如何获取。这个机制称为变量对象(variable object)。

变量对象(variable object,简写为VO)是一个与执行上下文相关的特殊对象,它储存了在上下文中声明的:

o 变量(变量声明VariableDeclaration,简写为var)

o 函数声明(FunctionDeclaration,简写为FD)

o 以及函数的形参(formal parameters)

注意:在ES5中,变量对象的概念被词法环境模型(lexical environments model)所替代。

我们可以用伪代码来将变量对象看作一个普通的ECMAScript对象:

  1. VO = {};

并且我们说,VO是执行上下文的一个属性:

  1. activeExecutionContext = {
  2. VO: {
  3. //上下文数据(var, FD, 函数参数)
  4. }
  5. }

只有在全局上下文中(变量对象是全局对象本身)我们才可以通过引用变量对象来间接引用变量(作为VO的属性名)。而其他上下文中不能直接引用变量对象,相关引用过程是实现器(implementation,译者注:即各js引擎)的机制。

当我们声明一个变量或函数时,我们所做的其实只是为VO创建了相应名和值的属性。例如:

  1. var a =10;
  2. function test(x) {
  3. var b = 20;
  4. };
  5. test(30);

而相应的变量对象为:

  1. VO(globalContext) = {
  2. a: 10,
  3. test: <reference to function>
  4. };
  5. VO(test functionContext) = {
  6. b: 20,
  7. x: 30
  8. };

但从实现器的角度(和规范)上说,变量对象是一个抽象概念。在物理上,变量对象在具体的执行上下文中有不同的名称和不同的初始化结构。

不同执行上下文中的变量对象 | Variable object in different execution contexts

变量对象的一些操作(比如变量实例化)和行为是所有执行上下文类型中都一致的。从这个角度上说,我们可以简单地把它看做是抽象基础类,函数和全局上下文中的变量对象在此基础上扩展了更多的细节:

  1. AbstractVO (generic behavior of the variable instantiation process)
  2. ╠══> GlobalContextVO
  3. (VO === this === global)
  4. ╚══> FunctionContextVO

(VO === AO,object andare added)
让我们来更详细地考虑这些情况。

全局上下文中的变量对象 | Variable object in global context

在这里,首先需要给出全局对象(Global object)的定义:

全局对象是在进入任何执行上下文之前就已经创建的对象。它的存在具有唯一性,它的属性在程序的任何地方都可以访问,它的生命周期在程序结束时结束。

在创建时,全局对象的初始化伴随生成一些属性,比如Math、String、Date、parseInt等,以及一些额外的,能访问全局对象自身的对象——例如BOM中全局对象的window属性引用全局对象自身(然而并不是在所有实现器中都是如此。译者注:比如旧版本的firebug控制台中this === _FirebugCommandLine,这个问题在最新的1.9版中已经修正):

  1. global = {
  2. Math: <...>,
  3. String: <...>,
  4. ...
  5. ...
  6. window: global
  7. };

当引用全局对象的属性时前缀常常被省略,因为全局对象不能直接通过名字来访问。然而,可以通过全局上下文中this的值来访问它,也可以递归引用它本身(比如BOM中的window)。因此它的写法很简单:

  1. String(10); // global.String(10)
  2. window.a =10; // global.window.a =10 === global.a = 10
  3. this.b = 10; // global.b = 10

那么,回到全局上下文中的变量对象上——这里的变量对象就是全局对象本身。

  1. VO(globalContext) = global;

有必要准确地理解这一点,因为正是这个原因使得那些在全局上下文中定义的变量可以通过全局对象的属性来间接引用它们(例如当变量名事先未知的情况下):

  1. var a = new String("test");
  2. alert(a); // "test",直接引用,属性a在VO(globalContext)中
  3. alert(window["a"]); // "test",间接,通过window === global === VO(globalContext)
  4. alert(a === this.a); // true
  5. var aKey = 'a';
  6. alert(window[aKey]); // "test",间接,通过动态属性名window[aKey] === window["a"]

函数上下文中的变量对象 | Variable object in function context

关于函数的执行上下文中的情况——这里的VO不能直接访问,并且在那里它扮演的角色我们称之为活化对象(Activation object,简写为AO)。

  1. VO(functionContext) === AO;

活化对象在进入一个函数的上下文时创建,并且初始化时伴随属性arguments——属性值是相应的参数对象(Arguments object):

  1. AO = {
  2. arguments: <ArgO>
  3. };

Arguments对象是活化对象的一个属性。它本身包括了如下属性:

o callee——当前函数的引用

o length——实际传入参数的个数

o properties-indexes(属性索引,整数,转换成了字符串)——属性值为索引相对的函数参数(参数列表中从左至右索引)的值。属性索引的数量==arguments.length。arguments对象的属性索引的值是和相应位置的形参共用的。(一个改变另一个也改变)

例如:

  1. function foo(x, y, z) {
  2. alert(foo.length); // 3, 形参个数
  3. alert(arguments.length); // 2, 实参个数
  4. alert(arguments.callee === foo); // true
  5. alert(x === arguments[0]); // true
  6. alert(x); 10
  7. arguments[0] = 20;
  8. alert(x); // 20
  9. x = 30;
  10. alert(arguments[0]); // 30
  11. //然而,由于没有传入第三个参数,因此arguments[2]和z的值不是共享的
  12. z = 40;
  13. alert(arguments[2]); // "undefined"
  14. arguments[2] = 50;
  15. alert(z); // 40
  16. }
  17. foo(10, 20);

关于上面的最后一种情况,旧版本Google Chrome中有一个bug——参数z和arguments[2]仍然共用了。

注意,在ES5中活化对象的概念也被通用和唯一的词法环境模型(lexical environments model)所替代了。

上下文代码的处理阶段 | Phases of processing the context code

现在我们来到了这篇文章的重点部分。执行上下文代码的处理分为两个基本阶段:

1、进入执行上下文;

2、执行代码。

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

注意,这两个阶段的处理是一般行为(general behavior),因此独立于上下文的类型之外(也就是说,在全局和函数上下文中它的行为是一样的)。

进入执行上下文 | Entering the execution context

在进入执行上下文的阶段(但在执行代码之前),变量对象中加入下列属性(正如在文章开头我们提到过的):

o 对于一个函数的每一个的形参(formal parameter)(如果在函数执行上下文中)

——变量对象中创建了一个拥有该形参名和值的属性;对于没有传入的参数,VO创建了名为形参名,值为undefined的属性;

o 对于每一个函数声明(FunctionDeclaration, FD)

——变量对象中创建了一个名为函数名,值为函数对象的属性;如果变量对象中已经有了相同名称的属性,替换它的值和参数;

o 对于每一个变量声明(VariableDeclaration, var)

——变量对象中创建了一个名为变量名,值为undefined的属性;如果变量名和已声明的形参或函数名相同,该变量的声明不影响已有的属性

让我们来看一个例子:

  1. function test(a, b) {
  2. var c = 10;
  3. function d() {}
  4. var e = function _e() {};
  5. (function x() {});
  6. }
  7. test(10);

在进入test函数的上下文并传入参数10的阶段,AO如下:

  1. AO = {
  2. a: 10,
  3. b: undefined,
  4. c: undefined,
  5. d: <reference to FunctionDeclaration "d">,
  6. e: undefined
  7. };

注意,AO并不包含函数x。这是因为x不是一个函数声明(function declaration)而是一个函数表达式(function-expression,简写为FE),而后者不影响VO。

然而,_e也是一个函数表达式,但我们将在下面看到,由于它被赋值给变量e,它变成可以通过名字e来访问。函数声明和函数表达式的详细区别将在第五章.函数中讨论到。

这一步完成后接下来是上下文代码处理的第二阶段——代码执行阶段。

代码执行 | Code execution

在这一刻,AO/VO已经拥有了相关的属性(虽然,并不是所有的都获得了我们传递的值,大部分仍然只有初始化值undefined)。

以上面的例子而言,AO/VO在代码解析中的改动如下:

  1. AO["c"] = 10;
  2. AO["e"] = <reference to FunctionExpression "_e">;

我们再一次注意到,函数表达式 _e 仍然在内存中,唯一原因是它被储存到了已声明的变量e上。但函数表达式x不在AO/VO内。如果我们试图在定义前(甚至定义后)调用x函数,我们会看到一个error: “x” is not defined. 在没有保存到变量上时,一个函数表达式只有在它定义时(e.g. (function x(){})();)或递归时才能调用。

另一个经典的例子是:

  1. alert(x); // function, 函数声明在第一阶段完成,而此时变量只声明未赋值
  2. var x = 10;
  3. alert(x); // 10
  4. x = 20;
  5. function x() {}
  6. alert(x); // 20

为什么第一个alert返回的x是function,并且在声明前就可以访问?为什么它不是10或者20?因为,根据规则——在进入上下文的阶段中,VO处理了函数声明;在同一阶段,也处理了变量声明x,但是正如我们前面所说的,变量声明的步骤在函数和形参声明之后,并且在这一阶段,(变量的声明)不会影响已声明的同名函数或形参的值。因此,在进入上下文阶段VO的改变如下:

  1. VO = {};
  2. VO['x'] = <reference to FunctionDeclaration 'x'>;
  3. //查找到var x = 10;
  4. //如果 function "x"未定义,
  5. //则VO['x']=="undefined", 注意与未声明时的调用报错“x is not defined”相区别
  6. //但由于function "x"已定义,变量声明不会影响同名函数的值
  7. VO['x'] = <reference to FunctionDeclaration 'x'>;
  8. 在这之后的代码执行阶段中,VO的改变如下:
  9. VO['x'] = 10;
  10. VO['x'] = 20;

这正是我们在第二和第三个alert中看到的结果。
从下面的例子中我们可以再一次看到,在进入上下文阶段变量声明就进入了VO之中(所以,即使else块永远不会执行,变量b却已经存在于VO中了):

  1. if (true) {
  2. var a = 1;
  3. } else {
  4. var b = 2;
  5. }
  6. alert(a); // 1
  7. alert(b); // "undefined", but not an error:"b is not defined"

关于变量 | About variables

许多关于Javascript的文章(甚至书)中都宣称:”声明一个全局变量可以通过‘var’关键字(在全局上下文中),或者不使用var关键字(在任何地方)”。这是错误的。记住:

变量只能通过var关键字来声明。

而像下面这种赋值的情况:

  1. a = 10;

只是创建了一个全局对象的新属性(但不是变量,译者按:因此可以被删除)。”不是变量”并不意味着它不能被改变,而是意味着它不是ECMAScript中变量的概念。(正如我们之前所说的,变量也会成为全局对象的属性,因为VO(globalContext) == global)。

让我们用一个例子来说明这两者的区别:

  1. alert(a); // "undefined"
  2. alert(b); // ReferenceError: "b is not defined"
  3. b = 10;
  4. var a = 20;

这一切都还是依赖于VO和它的改变的两个阶段(进入上下文阶段和执行代码阶段):

进入上下文阶段:

  1. VO = {
  2. a: undefined
  3. };

我们看到这一阶段中并没有b,因为它不是一个变量。b将在代码执行阶段出现(但由于这里报出error因此不会执行下去)。
让我们改变一下代码:

  1. alert(a); // undefined
  2. b = 10;
  3. alert(b); // 10, 在代码执行阶段创建
  4. var a = 20;
  5. alert(a); // 20, 在代码执行阶段改变

关于变量,这里还有一个重要的概念:与一般属性不同,变量拥有内部参数{DontDelete},因此不能通过delete运算符删除一个变量(译者按:更多细节见理解javascript中的delete一文):

  1. a = 10;
  2. alert(window.a); // 10
  3. alert(delete window.a); // true
  4. alert(window.a); // undefined
  5. var b = 20;
  6. alert(window.b); // 20
  7. alert(delete window.b); // false
  8. alert(window.b); // 20

注意:在ES5中{DontDelete}被重命名为[[Configurable]],并且可以通过Object.defineProperty方法手动管理。

实现器的特性:parent属性 | Feature of implementations: property parent

正如前面提到过的,以标准而言,直接访问活化对象(activation object)是不可能的。然而在一些实现器中,例如SpiderMonkey(firefox早期的js引擎)和Rhino,函数一个特殊的属性parent,这个属性引用相应的活化对象(或全局变量对象)。

例如(在SpiderMonkey和Rhino中):

  1. var global = this;
  2. var a = 10;
  3. function foo() {}
  4. alert(foo.__parent__); // global
  5. var VO = foo.__parent__;
  6. alert(VO.a); // 10
  7. alert(VO === global); // true

上面的例子中我们看到foo函数在全局上下文中创建,因此它的parent属性指向全局上下文中的变量对象,即全局对象。
然而,在SpiderMonkey中却不能用同样的方法去获取活化对象:根据不同版本,函数内的parent返回null或者全局对象。
在Rhino中可以使用同样的方法获取活化对象:

  1. var global = this;
  2. var x = 10;
  3. (function foo() {
  4. var y = 20;
  5. // "foo"上下文的活化对象
  6. var AO = (function () {}).__parent__;
  7. print(AO.y); // 20
  8. // 当前的活化对象的__parent__是全局对象,
  9. // 换句话说,形成了一个特殊的变量对象的链,即作用域链scope chain
  10. print(AO.__parent__ === global); // true
  11. print(AO.__parent__.x); // 10
  12. })();

总结 | Conclusion

在这一章里,我们进一步学习了与执行上下文相关的对象,我希望这些内容能够澄清一些过去可能存在的模糊的地方。按计划,下面的章节将会讲到作用域链(Scope chain),标示符处理(Identifier resolution)和闭包(Closure)。

版权声明:本站文章除特别声明外,均采用署名-非商业性使用-禁止演绎 4.0 国际 许可协议,如需转载,请注明出处
  • ECMA-262-3 in detail——第七章:OOP(第二部分:ECMAScript实现)

    这一章的第二部分是关于EMCAScript中的面向对象编程。在第一部分中我们讨论了OOP的基本理论并勾画出和ECMAScript的相似之处。在阅读第二部分之前,如果有必要,我还是建议首先阅读这一章的第一部分.基本理论,因为后面将会用到其中的一些术语。

    发布:2015-05-26 阅读(2114)

  • ECMA-262-3 in detail——第七章:OOP(第一部分:一般理论)

    这一章我们讨论ECMAScript中面向对象编程(object-oriented programming)的几个主要方面。由于这一主题已经在许多文章中谈论过,本章并不打算“老调重弹”,而是试图更多地着眼于这些过程内在的理论方面。尤其是,我们将研究对象创建的算法,看看对象间的关系(包括最基本的关系——继承)是如何实现的,并且给出一些讨论中将用到的准确定义(我希望这样能够打消一些术语和思路上的疑惑以及一些关于Javascript文章中OOP部分的常见的混淆)。

    发布:2015-05-06 阅读(1872)

  • ECMA-262-3 in detail——第六章:闭包

    在这一章中我们来谈谈Javascript中被讨论最多的话题之一——关于闭包(closures)。事实上这个主题并不是新鲜的。然而我们在这里将试着更多从理论的角度去分析和理解它,然后我们还会看一下ECMAScript内关于闭包的内容。

    发布:2015-04-28 阅读(1896)

  • ECMA-262-3 in detail——第五章:函数

    在这章里我们讨论ECMAScript中的一个基本对象——函数。我们将会看到不同类型的函数如何影响一个上下文中的变量对象,以及这些函数的作用域链中都包含什么。我们将会回答像下面这样经常被问到的问题:“下面这两种创建函数的方式有什么区别吗(如果有的话,区别是什么呢)?”

    发布:2015-04-17 阅读(2019)

  • ECMA-262-3 in detail——第四章:作用域链

    正如我们从第二章.变量对象中了解到的,执行上下文的数据(变量,函数声明,函数形参)以变量对象的属性的方式储存。

    发布:2015-04-07 阅读(1825)

  • ECMA-262-3 in detail——第三章:this关键字

    许多程序员习惯于认为在编程语言中,this关键字是与面向对象编程紧密相关的,而且引用的是由构造式最新创建的对象。在ECMAScript中,这个概念也被实现了,然而我们将看到,在这里它不仅仅指向已创建的对象。

    发布:2015-03-25 阅读(2038)

  • ECMA-262-3 in detail——第二章:变量对象

    在程序中我们总是声明变量和函数然后用它们来搭建我们的系统。但是解释器(interpreter)是在哪里和以什么方式来找到我们的数据(函数,变量)的呢?

    发布:2015-03-24 阅读(2140)

  • ECMA-262-3 in detail——第一章:执行上下文

    第1章:在这一章里,我们将会讨论ECMAScript中的执行上下文(execution context)以及与它们相关的可执行代码(executable code)的类型。

    发布:2015-01-10 阅读(2082)