从来没有深入了解ECMA,网上找了一下,发现早在2010年就有大佬 Dmitry Soshnikov 总结了ECMA中的核心内容,我这里只是翻译记录,加深自己的印象。文章原文来自 ECMA-262-3 in detail. Chapter 2. Variable object.
在我们创建应用程序的时候,总是避免不了会进行函数和变量的声明。但是,解释器(interpreter)是怎么找到我们的数据(函数和变量)的呢?又是在哪里找到的呢?我们引用我们需要的对象的时候又发生了什么呢?
大部分程序员都知道变量与执行上下文紧密相关。
var a = 10; // variable of global context
(function foo() { var b = 20; // local variable of the function context})();
alert(a); // 10alert(b); // b is not defined
同样,许多程序员也都知道,在当前的版本规范中,只有函数(function)代码的执行上下文才可以创建独立的作用域。与C++等语言相反,在ECMAScript中,for循环 没有
创建一个独立的作用域。这就是为什么,下面的代码, i
始终是5
var obj = {};for (var i = 0; i < 5; i++) { obj[i] = function() {console.log(i)}}
for (var k in obj) { obj[k]() }
我们来详细了解一下声明数据的时候都发生了什么。
如果变量与执行上下文相关,那么,他就知道它的数据存放在哪里以及如何获取。这种机制称为 变量对象(variable object)
一个变量对象(缩写形式 - VO)是包含执行上下文的特殊对象,并且保存着:
var
, 变量声明 )以上内容均在上下文中声明。
Notice:在ES5中,变量对象的概念已经被词汇环境模型所取代。
举例来说,可以将变量对象表示为普通的ESMAScript对象:
VO = {};
正如我们所说,变量对象是执行上下文的一个属性,则:
activeExecutionContext = { VO: { // context data(var, FD, function arguments) }}
只有全局上下文中的变量对象可以通过VO的属性名称间接访问、使用(其中全局变量自身就是变量对象)。对于其他的上下文,直接访问VO是不可能的,因为它(VO)纯粹是实现机制(内部的事情)。
当我们声明变量或者函数的时候,除了使用变量名和值创建VO的新属性外,没有其他的事情了。
例如:
var a = 10;function test(x) { var b = 20;};test(30);
对应的变量对象是:
// 全局的变量对象VO(globalContext) = { a: 10, test: <test> fn}
// 函数 test 中的变量对象VO(test functionContext) = { x: 30, b: 20}
但是在具体的实现层级(和规范中),变量对象只是抽象的事物(实际上是不存在的)。从根本上来说,在不同的具体执行上下文中,VO的名称和初始结构都是不同的。
变量对象的某些操作(例如:变量实例化)和表现对于所有的执行上下文类型都成很普通的。从这个角度出发,将变量对象当作为一个抽象的基础物质更容易理解。函数上下文还可以定义域变量对象相关的其他详细信息。
AbstractVO (变量实例化过程的一般行为) ║ ╠══> GlobalContextVO ║ (VO === this === global) ║ ╚══> FunctionContextVO (VO === AO, <arguments> object and <formal parameters> are added)
我们来详细分析一下:
首先,要给出Global对象的定义:
全局对象是在进入任何执行上下文之前就被创建好的。这个对象只存在一份,他的属性可以在进程的任何地方访问,进程结束,全局对象的声明周期结束。
在创建时候,全局对象通过 Math
, String
, Date
, parseInt
等属性进行初始化,还可以附加其他对象作为属性,其中也包括引用全局对象自身的对象。例如:在BOM(浏览器对象模型)中,全局对象的 window
属性就是指向全局的(当然,并不是所有的实现都是这样的)。
请看下面的这个例子:windos
是global的属性,但同时值是global。
global = { Math: <...>, String: <...>, ..., window: global}
当引用全局对象属性的时候,通常是省略前缀的,因为全局对象不可以直接通过名称访问。但是,可以通过全局上下文中的 this
访问,也可以通过递归自己调用自己(例如BOM中的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中找到了:'text'
alert(window['a']); // 间接获取到,通过 global === VO: 'text'alert(a === this.a); // true this === window
var akey = 'a';alert(window[akey]); // 间接获取到,通过动态属性名 akey === 'a'
关于函数的执行上下文,VO是不能直接获取的。此时由活动对象(activation object)扮演VO的角色。
VO(functionContext) === AO;
活动对象在进入函数上下文的时候被创建,并且有一个属性名为 argumants
,属性值为 Argumants Object
的初始值:
AO = { arguments: <ArgO>}
Arguments Object
是活动对象的属性,他包含以下属性:
function foo(x, y, z) { // 定义的函数的参数数量 (x, y, z) alert(foo.length); // 3 // 实际传递参数的数量 (only x, y) alert(arguments.length); // 2 // 函数自身的引用 alert(arguments.callee === foo); // true // 参数共享 alert(x === arguments[0]); // true alert(x); // 10 arguments[0] = 20; alert(x); // 20 11549 x = 30; alert(arguments[0]); // 30 // 但是,对于未传递参数的z,属性索引的值是不共享的 z = 40; alert(arguments[2]); // undefined arguments[2] = 50; alert(z); // 40 } foo(10, 20);
关于最后一个例子,在 chrome 的老版本中存在一个bug — 即,没有传递参数z,z 与 arguments[2] 的仍然是共享的。
现在,我们终于进入到本文的关键部分,处理上下文代码的过程被分为两个基本阶段:
变量对象的修改与这两个阶段有着密切的关联。
注意:这两个阶段的处理是一般行为,与上下文类型无关。(对于全局和函数上下文都是公平的)。
当进入执行上下文(但是是在代码运行 之前
),VO被下面这些属性填充(在前文已经描述过)(从上往下优先级依次降低)
undefined
的值组成。undefined
。如果这个变量名和已经声明的形参或者是函数名称相同,那么这个变量不会影响已经存在的属性。来看一个例子:
function test(a, b) { var c = 10; function d() {} var e = function _e() {} (function x(){})}test(10);
当进入到 test 函数的上下文的时候,test函数接收了一个实参 10,AO对象如下:
AO(test) = { a: 10, b: undefined, c: undefined, d: <FD d>, e: undefined}
注意,AO中并没有包含函数 x,这是因为 x 并不是一个函数声明而是一个函数表达式(FunctionExpression, 缩写形式:FE),不影响VO(即这里的AO)。
但是,函数 _e 也是一个函数表达式,就像接下来要看到的,它是被分配给了变量 e,它可以通过变量 e 来访问。关于函数声明( FunctionDeclaration
)和函数表达式(FunctionExpression
)的不同将会在Chapter 5. Functions中讲到。
这个时候,AO/VO已经被各种属性填满了(但是,不是所有的属性都已经有具体的值了,他们中的大部分的初始值都还是 undefined
)。
所有代码以及环境不变的情况下,上面的代码中,AO/VO在代码解释器间被修改为如下:
AO['c'] = 10;AO['e'] = <FE _e>;
再次注意,因为FE _e 是被保存在变量 e 中,所以,它仍然存在于内存(理解成AO/VO)中。但是FE x 不在了。如果我们在定义之前或者时候调用 x 函数,我们会得到一个错误:x is not defined
。没有保存到一个变量的函数表达式(FE)只能立即执行或者是递归调用。
另一个经典例子:
alert(x); // functionvar x = 10;alert(x); // 10x = 20;function x() {};alert(x); // 20
为什么第一次 alert x
的是一个函数,而且,还是在声明之前?为什么不是 10
或者 20
?因为,根据规则 — 当进去执行上下文的时候,VO是由函数声明填充的。同时,在相同的阶段,进入执行上下文的时候,有一个 x 的变量声明,但是我们上面已经提到了,如果这个变量名和已经声明的形参或者是函数名称相同,那么这个变量不会影响已经存在的属性。因此,当进入执行上下文的时候。VO进行如下填充:
VO = {};VO['x'] = <FD X>;// 找到x的变量声明,但是x已经存在了,所以变量声明无效VO['x'] = <值没有被影响,仍然是 function x>
当到了函数执行阶段,VO进行如下填充:
VO['x'] = 10;VO['x'] = 20;
这就是我们在第二次 alert 和第三次 alert 看到的内容。
下面的例子中,我们看到,当进入执行上下文阶段的时候变量都被存放在了 VO 中(虽然else
语句块没有执行,但是, b
依然存在于 VO中)
if (true) { var a = 1;} else { var b = 2;}
alert(a); // 1alert(b); // undefined 不是 b is not defined.
通常很多关于JavaScript的文章或者数据中都指出:“不管是使用var关键字(在全局上下文)还是不使用var关键字(在任何地方),都可以声明一个变量”。根本就不是这样的。请记住:
变量声明只能通过 var 关键字进行声明。
就像这样:
a = 10;
这仅仅只是在全局对象上创建了一个新的属性(而不是一个变量)。“不是变量”不是表示不能被修改,而是指ESMAScript规范中的“不是变量”。(由于 VO(globalContext) === global的原因,也会成为全局对象上的属性,还记得吗?)
让我们用代码来展示两者的不同
alert(a); // undefinedalert(b); // b is not defined
b = 10;var a = 20;
所有这些都取决于VO及其修改的阶段(进入上下文阶段和代码执行阶段):
进入上下文:
VO = { a: undefined};
我们可以看到,这里没有 b
,因为这不是一个变量。b
只会出现在在代码执行阶段(但是上面的例子中不会出现,因为出错了)。
来改一下这段代码:
alert(a); // undefined 我们知道为什么
b = 10;alert(b); // 10 创建 代码执行阶段
var a = 20;alert(a); // 20 代码执行阶段修改
关于变量还有一个很重要的观点。与简单属性相反,变量具有 DontDelete
属性(ES5中为 [[Configurable]]
),意味着我们不能通过 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}
特性,可以被删除。
前面已经提到过,按照标准规范,活动对象是不能直接访问的。然而,一些具体的实现并没有按照这个标准,例如 SpiderMonkey
和Rhino。在这些实现中,函数具有特殊的属性 __parent__
,通过这个属性可以访问到已经创建的活动对象(或者是全局变量对象)。
例如(SpiderMonkey, Rhino):
var global = this;var a = 10;function foo() {};alert(foo.__parent__); // globalvar VO = foo.__parent__;alert(VO.a); // 10alert(VO === global);
在上面的例子中我们可以看到,函数foo是在全局上下文中创建的,所以属性__parent__ 指向全局上下文的变量对象,即全局对象。(译者注:还记得这个吧:VO(globalContext) === global)
然而,在SpiderMonkey中用同样的方式访问激活对象是不可能的:在不同版本的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 })();
本文系外文翻译,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文系外文翻译,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。