提要
我们老是会在顺序中定义一些函数和变量,以后会运用这些函数和变量来构建我们的体系。
然则,关于诠释器来讲,它又是怎样以及从那里找到这些数据的(函数,变量)?当援用一个对象的时刻,在诠释器内部又发作了什么?
很多ECMA剧本顺序员都晓得,变量和实行高低文是密切相干的:
var a = 10; // 全局高低文中的变量
(function () {
var b = 20; // 函数高低文中的局部变量
})();
alert(a); // 10
alert(b); // "b" is not defined
</pre>
不仅云云,很多顺序员也都晓得,ECMAScript规范中指出自力的作用域只要经由历程“函数代码”(可实行代码范例中的一种)才建立出来。比方说,与C/C++差别的是,在ECMAScript中for轮回的代码块是没法建立当地高低文的:
<pre>for (var k in {a: 1, b: 2}) {
alert(k);
}
alert(k); // 只管轮回已完毕,然则变量“k”仍然在作用域中
下面就来细致引见下,当声明变量和函数的时刻,终究发作了什么。
数据声明
既然变量和实行高低文有关,那它就该晓得数据存储在那里以及怎样猎取。这类机制就称作变量对象:
A variable object (in abbreviated form — VO) is a special object related with an execution context and which stores:
variables (var, VariableDeclaration);
function declarations (FunctionDeclaration, in abbreviated form FD);
and function formal parameters
declared in the context.
举个例子,可以用ECMAScript的对象来示意变量对象:
<pre>VO = {};
</pre>
VO同时也是一个实行高低文的属性:
<pre>activeExecutionContext = {
VO: {
// 高低文中的数据 (变量声明(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”函数高低文中的变量对象 VO(test functionContext) = { x: 30, b: 20 }; 然则,在完成层(规范中定义的),变量对象只是一个笼统的观点。在现实实行高低文中,VO可以完整不叫VO,而且初始的构造也可以完整差别。
差别实行高低文中的变量对象
变量对象上的一些操纵(比方:变量的初始化)和行动关于一切的实行高低文范例来讲都已一样的。从这一点来讲,将变量对象示意成笼统的观点越发适宜。 函数高低文还能定义分外的与变量对象相干的信息。
AbstractVO (generic behavior of the variable instantiation process) ║ ╠══> GlobalContextVO ║ (VO === this === global) ║ ╚══> FunctionContextVO (VO === AO, object and are added)
接下来对这块内容举行细致引见。
全局高低文中的变量对象
起首,有必要对全局对象(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); // 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是不能直接接见的。它重要饰演被称作活泼对象(activation object)(简称:AO)的角色。
VO(functionContext) === AO;
活泼对象会在进入函数高低文的时刻建立出来,初始化的时刻会建立一个arguments属性,其值就是Arguments对象:
AO = { arguments: };
Arguments对象是活泼对象上的属性,它包括了以下属性:
- callee —— 对当前函数的援用
- length —— 实参的个数
- properties-indexes(数字,转换成字符串)其值是函数参数的值(参数列表中,从左到右)。properties-indexes的个数 == arguments.length;
arguments对象的properties-indexes的值和当前(现实通报的)形参是同享的。
以下所示:
function foo(x, y, z) { // 定义的函数参数(x,y,z)的个数 alert(foo.length); // 3 // 现实通报的参数个数 alert(arguments.length); // 2 // 援用函数本身 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, // 相干的arguments对象的index-property是不同享的 z = 40; alert(arguments[2]); // undefined arguments[2] = 50; alert(z); // 40 } foo(10, 20);
上述例子,在当前的Google Chrome浏览器中有个bug——参数z和arguments[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
当以10为参数进入“test”函数高低文的时刻,对应的AO以下所示:
AO(test) = { a: 10, b: undefined, c: undefined, d: e: undefined };
注重了,上面的AO并不包括函数“x”。这是因为这里的“x”并不是函数声明而是函数表达式(FunctionExpression,简称FE),函数表达式不会对VO形成影响。 只管函数“_e”也是函数表达式,然则,正如我们所看到的,因为它被赋值给了变量“e”,因而它可以经由历程“e”来接见到。关于函数声明和函数表达式的区分会在第五章——函数作详细引见。
至此,处置惩罚高低文代码的第一阶段引见完了,接下来引见第二阶段——实行代码阶段。
实行代码
此时,AO/VO的属性已添补好了。(只管,大部分属性都还没有给予真正的值,都只是初始化时刻的undefined值)。
继承以上一例子为例,到了实行代码阶段,AO/VO就会修正成为以下情势:
AO['c'] = 10; AO['e'] = ;
再次注重到,这里函数表达式“_e”仍在内存中,这是因为它被保留在声明的变量“e”中,而一样是函数表达式的“x”却不在AO/VO中: 假如尝试在定义前或许定义后挪用“x”函数,这时候会发作“x为定义”的毛病。未保留的函数表达式只要在定义或许递归时才挪用。
以下是越发典范的例子:
alert(x); // function var x = 10; alert(x); // 10 x = 20; function x() {}; alert(x); // 20
上述例子中,为什么“x”打印出来是函数呢?为什么在声明前就可以接见到?又为什么不是10或许20呢?缘由在于,依据划定规矩——在进入高低文的时刻,VO会被添补函数声明; 统一阶段,另有变量声明“x”,然则,正云云前提到的,变量声明是在函数声明和函数形参以后,而且,变量声明不会对已存在的一样名字的函数声明和函数形参发作冲突, 因而,在进入高低文的阶段,VO添补为以下情势:
VO = {}; VO['x'] = // 发明var x = 10; // 假如函数“x”还未定义 // 则 "x" 为undefined, 然则,在我们的例子中 // 变量声明并不会影响同名的函数值 VO['x'] =
随后,在实行代码阶段,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的文章以至是JavaScript的书一般都邑这么说:“声明全局变量的体式格局有两种,一种是运用var关键字(在全局高低文中),别的一种是不必var关键字(在任何位置)”。 而如许的形貌是毛病的。要记着的是:
运用var关键字是声明变量的唯一体式格局
以下赋值语句:
a = 10;
仅仅是在全局对象上建立了新的属性(而不是变量)。“不是变量”并不意味着它没法转变,它是ECMAScript中变量的观点(它以后可以变成全局对象的属性,因为VO(globalContext) === global,还记得吧?)
差别点以下所示:
alert(a); // undefined alert(b); // "b" is not defined b = 10; var a = 20;
接下来照样要谈到VO和在差别阶段对VO的修正(进入高低文阶段和实行代码阶段):
进入高低文:
VO = { a: undefined };
我们看到,这个阶段并没有任何“b”,因为它不是变量,“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操纵符来删除一个变量是不可以的。
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”实行高低文中,是可以删除变量的:
eval('var a = 10;'); alert(window.a); // 10 alert(delete a); // true alert(window.a); // undefined
应用某些debug东西,在终端测试过这些例子的童鞋要注重了:个中Firebug也是运用了eval来实行终端的代码。因而,这个时刻var也是可以删除的。
完成层的特征: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
})();
总结
本文,我们引见了与实行高低文相干的对象。愿望,本文可以对人人有所协助,同时也愿望本文可以起到解惑的作用。
扩大浏览
此文译自Dmitry A.Soshnikov的文章Variable object
赵静/宋珍珍 译
via 前端翻译小站