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