深入理解JavaScript之變量對象(Variable Object)

英文 | http://dmitrysoshnikov.com/ecmascript/chapter-2-variable-object/
介紹
var a = 10; // 全局上下文中的變量(function () {var b = 20; // function上下文中的局部變量})();alert(a); // 10alert(b); // 全局變量 "b" 沒有聲明
并且,很多程序員也都知道,當(dāng)前ECMAScript規(guī)范指出獨立作用域只能通過“函數(shù)(function)”代碼類型的執(zhí)行上下文創(chuàng)建。也就是說,相對于C/C++來說,ECMAScript里的for循環(huán)并不能創(chuàng)建一個局部的上下文。
for (var k in {a: 1, b: 2}) {alert(k);}alert(k); // 盡管循環(huán)已經(jīng)結(jié)束但變量k依然在當(dāng)前作用域
我們來看看一下,我們聲明數(shù)據(jù)的時候到底都發(fā)現(xiàn)了什么細(xì)節(jié)。
數(shù)據(jù)聲明
如果變量與執(zhí)行上下文相關(guān),那變量自己應(yīng)該知道它的數(shù)據(jù)存儲在哪里,并且知道如何訪問。這種機(jī)制稱為變量對象(variable object)。
變量對象(縮寫為VO)是一個與執(zhí)行上下文相關(guān)的特殊對象,它存儲著在上下文中聲明的以下內(nèi)容:
變量 (var, 變量聲明);函數(shù)聲明 (FunctionDeclaration, 縮寫為FD);函數(shù)的形參
舉例來說,我們可以用普通的ECMAScript對象來表示一個變量對象:
VO = {};就像我們所說的, VO就是執(zhí)行上下文的屬性(property):
activeExecutionContext = {VO: {// 上下文數(shù)據(jù)(var, FD, function arguments)}};
只有全局上下文的變量對象允許通過VO的屬性名稱來間接訪問(因為在全局上下文里,全局對象自身就是變量對象,稍后會詳細(xì)介紹),在其它上下文中是不能直接訪問VO對象的,因為它只是內(nèi)部機(jī)制的一個實現(xiàn)。
當(dāng)我們聲明一個變量或一個函數(shù)的時候,和我們創(chuàng)建VO新屬性的時候一樣沒有別的區(qū)別(即:有名稱以及對應(yīng)的值)。
例如:
var a = 10;function test(x) {var b = 20;};test(30);
對應(yīng)的變量對象是:
// 全局上下文的變量對象VO(globalContext) = {a: 10,test: function>};// test函數(shù)上下文的變量對象VO(test functionContext) = {x: 30,b: 20};
在具體實現(xiàn)層面(以及規(guī)范中)變量對象只是一個抽象概念。(從本質(zhì)上說,在具體執(zhí)行上下文中,VO名稱是不一樣的,并且初始結(jié)構(gòu)也不一樣。
不同執(zhí)行上下文中的變量對象
對于所有類型的執(zhí)行上下文來說,變量對象的一些操作(如變量初始化)和行為都是共通的。從這個角度來看,把變量對象作為抽象的基本事物來理解更為容易。同樣在函數(shù)上下文中也定義和變量對象相關(guān)的額外內(nèi)容。
抽象變量對象VO (變量初始化過程的一般行為)║╠══> 全局上下文變量對象GlobalContextVO║ (VO === this === global)║╚══> 函數(shù)上下文變量對象FunctionContextVO(VO === AO, 并且添加了<arguments>和<formal parameters>)
我們來詳細(xì)看一下:
全局上下文中的變量對象
首先,我們要給全局對象一個明確的定義
全局對象(Global object) 是在進(jìn)入任何執(zhí)行上下文之前就已經(jīng)創(chuàng)建了的對象;
這個對象只存在一份,它的屬性在程序中任何地方都可以訪問,全局對象的生命周期終止于程序退出那一刻。
全局對象初始創(chuàng)建階段將Math、String、Date、parseInt作為自身屬性,等屬性初始化,同樣也可以有額外創(chuàng)建的其它對象作為屬性(其可以指向到全局對象自身)。
例如,在DOM中,全局對象的window屬性就可以引用全局對象自身(當(dāng)然,并不是所有的具體實現(xiàn)都是這樣):
global = {Math: <...>,String: <...>......window: global //引用自身};
當(dāng)訪問全局對象的屬性時通常會忽略掉前綴,這是因為全局對象是不能通過名稱直接訪問的。
不過我們依然可以通過全局上下文的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;非常有必要要理解上述結(jié)論,基于這個原理,在全局上下文中聲明的對應(yīng),我們才可以間接通過全局對象的屬性來訪問它(例如,事先不知道變量名稱)。
var a = new String('test');alert(a); // 直接訪問,在VO(globalContext)里找到:"test"alert(window['a']); // 間接通過global訪問:global === VO(globalContext): "test"alert(a === this.a); // truevar aKey = 'a';alert(window[aKey]); // 間接通過動態(tài)屬性名稱訪問:"test"
函數(shù)上下文中的變量對象
在函數(shù)執(zhí)行上下文中,VO是不能直接訪問的,此時由活動對象(activation object,縮寫為AO)扮演VO的角色。
VO(functionContext) === AO;活動對象是在進(jìn)入函數(shù)上下文時刻被創(chuàng)建的,它通過函數(shù)的arguments屬性初始化。arguments屬性的值是Arguments對象:
AO = {arguments: <ArgO>};
Arguments對象是活動對象的一個屬性,它包括如下屬性:
callee — 指向當(dāng)前函數(shù)的引用
length — 真正傳遞的參數(shù)個數(shù)
properties-indexes (字符串類型的整數(shù)) 屬性的值就是函數(shù)的參數(shù)值(按參數(shù)列表從左到右排列)。properties-indexes內(nèi)部元素的個數(shù)等于arguments.length. properties-indexes 的值和實際傳遞進(jìn)來的參數(shù)之間是共享的。
例如:
function foo(x, y, z) {// 聲明的函數(shù)參數(shù)數(shù)量arguments (x, y, z)alert(foo.length); // 3// 真正傳進(jìn)來的參數(shù)個數(shù)(only x, y)alert(arguments.length); // 2// 參數(shù)的callee是函數(shù)自身alert(arguments.callee === foo); // true// 參數(shù)共享alert(x === arguments[0]); // truealert(x); // 10arguments[0] = 20;alert(x); // 20x = 30;alert(arguments[0]); // 30// 不過,沒有傳進(jìn)來的參數(shù)z,和參數(shù)的第3個索引值是不共享的z = 40;alert(arguments[2]); // undefinedarguments[2] = 50;alert(z); // 40}foo(10, 20);
這個例子的代碼,在當(dāng)前版本的Google Chrome瀏覽器里有一個bug — 即使沒有傳遞參數(shù)z,z和arguments[2]仍然是共享的。
處理上下文代碼的2個階段
現(xiàn)在我們終于到了本文的核心點了。執(zhí)行上下文的代碼被分成兩個基本的階段來處理:
進(jìn)入執(zhí)行上下文
執(zhí)行代碼
變量對象的修改變化與這兩個階段緊密相關(guān)。
注:這2個階段的處理是一般行為,和上下文的類型無關(guān)(也就是說,在全局上下文和函數(shù)上下文中的表現(xiàn)是一樣的)。
進(jìn)入執(zhí)行上下文
當(dāng)進(jìn)入執(zhí)行上下文(代碼執(zhí)行之前)時,VO里已經(jīng)包含了下列屬性(前面已經(jīng)說了):
函數(shù)的所有形參(如果我們是在函數(shù)執(zhí)行上下文中)
— 由名稱和對應(yīng)值組成的一個變量對象的屬性被創(chuàng)建;沒有傳遞對應(yīng)參數(shù)的話,那么由名稱和undefined值組成的一種變量對象的屬性也將被創(chuàng)建。
所有函數(shù)聲明(FunctionDeclaration, FD)
—由名稱和對應(yīng)值(函數(shù)對象(function-object))組成一個變量對象的屬性被創(chuàng)建;如果變量對象已經(jīng)存在相同名稱的屬性,則完全替換這個屬性。
所有變量聲明(var, VariableDeclaration)
— 由名稱和對應(yīng)值(undefined)組成一個變量對象的屬性被創(chuàng)建;如果變量名稱跟已經(jīng)聲明的形式參數(shù)或函數(shù)相同,則變量聲明不會干擾已經(jīng)存在的這類屬性。
讓我們看一個例子:
function test(a, b) {var c = 10;function d() {}var e = function _e() {};(function x() {});}test(10); // call
當(dāng)進(jìn)入帶有參數(shù)10的test函數(shù)上下文時,AO表現(xiàn)為如下:
AO(test) = {a: 10,b: undefined,c: undefined,d: <reference to FunctionDeclaration "d">e: undefined};
注意,AO里并不包含函數(shù)“x”。這是因為“x” 是一個函數(shù)表達(dá)式(FunctionExpression, 縮寫為 FE) 而不是函數(shù)聲明,函數(shù)表達(dá)式不會影響VO。
不管怎樣,函數(shù)“_e” 同樣也是函數(shù)表達(dá)式,但是就像我們下面將看到的那樣,因為它分配給了變量 “e”,所以它可以通過名稱“e”來訪問。
函數(shù)聲明FunctionDeclaration與函數(shù)表達(dá)式FunctionExpression 的不同,我們會在Functions進(jìn)行詳細(xì)的探討,也可以參考《深入理解JavaScript之揭秘命名函數(shù)表達(dá)式》來了解。
這之后,將進(jìn)入處理上下文代碼的第二個階段 — 執(zhí)行代碼。
代碼執(zhí)行
這個周期內(nèi),AO/VO已經(jīng)擁有了屬性(不過,并不是所有的屬性都有值,大部分屬性的值還是系統(tǒng)默認(rèn)的初始值undefined )。
還是前面那個例子, AO/VO在代碼解釋期間被修改如下:
AO['c'] = 10;AO['e'] = <reference to FunctionExpression "_e">;
再次注意,因為FunctionExpression“_e”保存到了已聲明的變量“e”上,所以它仍然存在于內(nèi)存中。
而FunctionExpression “x”卻不存在于AO/VO中,也就是說如果我們想嘗試調(diào)用“x”函數(shù),不管在函數(shù)定義之前還是之后,都會出現(xiàn)一個錯誤“x is not defined”,未保存的函數(shù)表達(dá)式只有在它自己的定義或遞歸中才能被調(diào)用。
另一個經(jīng)典例子:
alert(x); // functionvar x = 10;alert(x); // 10x = 20;function x() {};alert(x); // 20
為什么第一個alert “x” 的返回值是function,而且它還是在“x” 聲明之前訪問的“x” 的?為什么不是10或20呢?因為,根據(jù)規(guī)范函數(shù)聲明是在當(dāng)進(jìn)入上下文時填入的;
同意周期,在進(jìn)入上下文的時候還有一個變量聲明“x”,那么正如我們在上一個階段所說,變量聲明在順序上跟在函數(shù)聲明和形式參數(shù)聲明之后,而且在這個進(jìn)入上下文階段,變量聲明不會干擾VO中已經(jīng)存在的同名函數(shù)聲明或形式參數(shù)聲明,因此,在進(jìn)入上下文時,VO的結(jié)構(gòu)如下:
VO = {};VO['x'] = <reference to FunctionDeclaration "x">// 找到var x = 10;// 如果function "x"沒有已經(jīng)聲明的話// 這時候"x"的值應(yīng)該是undefined// 但是這個case里變量聲明沒有影響同名的function的值VO['x'] = <the value is not disturbed, still function>
緊接著,在執(zhí)行代碼階段,VO做如下修改:
VO['x'] = 10;VO['x'] = 20;
我們可以在第二、三個alert看到這個效果。
在下面的例子里我們可以再次看到,變量是在進(jìn)入上下文階段放入VO中的。(因為,雖然else部分代碼永遠(yuǎn)不會執(zhí)行,但是不管怎樣,變量“b”仍然存在于VO中。)
if (true) {var a = 1;} else {var b = 2;}alert(a); // 1alert(b); // undefined,不是b沒有聲明,而是b的值是undefined
關(guān)于變量
通常,各類文章和JavaScript相關(guān)的書籍都聲稱:“不管是使用var關(guān)鍵字(在全局上下文)還是不使用var關(guān)鍵字(在任何地方),都可以聲明一個變量”。請記住,這是錯誤的概念:
任何時候,變量只能通過使用var關(guān)鍵字才能聲明。
上面的賦值語句:
a = 10;這僅僅是給全局對象創(chuàng)建了一個新屬性(但它不是變量)。“不是變量”并不是說它不能被改變,而是指它不符合ECMAScript規(guī)范中的變量概念,所以它“不是變量”(它之所以能成為全局對象的屬性,完全是因為VO(globalContext) === global,大家還記得這個吧?)。
讓我們通過下面的實例看看具體的區(qū)別吧:
alert(a); // undefinedalert(b); // "b" 沒有聲明b = 10;var a = 20;
所有根源仍然是VO和進(jìn)入上下文階段和代碼執(zhí)行階段:
進(jìn)入上下文階段:
VO = {a: undefined};
我們可以看到,因為“b”不是一個變量,所以在這個階段根本就沒有“b”,“b”將只在代碼執(zhí)行階段才會出現(xiàn)(但是在我們這個例子里,還沒有到那就已經(jīng)出錯了)。
讓我們改變一下例子代碼:
alert(a); // undefined, 這個大家都知道,b = 10;alert(b); // 10, 代碼執(zhí)行階段創(chuàng)建var a = 20;alert(a); // 20, 代碼執(zhí)行階段修改
關(guān)于變量,還有一個重要的知識點。變量相對于簡單屬性來說,變量有一個特性(attribute):{DontDelete},這個特性的含義就是不能用delete操作符直接刪除變量屬性。
a = 10;alert(window.a); // 10alert(delete a); // truealert(window.a); // undefinedvar b = 20;alert(window.b); // 20alert(delete b); // falsealert(window.b); // still 20
但是這個規(guī)則在有個上下文里不起走樣,那就是eval上下文,變量沒有{DontDelete}特性。
eval('var a = 10;');alert(window.a); // 10alert(delete a); // truealert(window.a); // undefined
使用一些調(diào)試工具(例如:Firebug)的控制臺測試該實例時,請注意,F(xiàn)irebug同樣是使用eval來執(zhí)行控制臺里你的代碼。因此,變量屬性同樣沒有{DontDelete}特性,可以被刪除。
特殊實現(xiàn): parent 屬性
前面已經(jīng)提到過,按標(biāo)準(zhǔn)規(guī)范,活動對象是不可能被直接訪問到的。但是,一些具體實現(xiàn)并沒有完全遵守這個規(guī)定,例如SpiderMonkey和Rhino;的實現(xiàn)中,函數(shù)有一個特殊的屬性 parent,通過這個屬性可以直接引用到活動對象(或全局變量對象),在此對象里創(chuàng)建了函數(shù)。
例如 (SpiderMonkey, Rhino):
var global = this;var a = 10;function foo() {}alert(foo.__parent__); // globalvar VO = foo.__parent__;alert(VO.a); // 10alert(VO === global); // true
在上面的例子中我們可以看到,函數(shù)foo是在全局上下文中創(chuàng)建的,所以屬性__parent__ 指向全局上下文的變量對象,即全局對象。
然而,在SpiderMonkey中用同樣的方式訪問活動對象是不可能的:在不同版本的SpiderMonkey中,內(nèi)部函數(shù)的__parent__ 有時指向null ,有時指向全局對象。
在Rhino中,用同樣的方式訪問活動對象是完全可以的。
例如 (Rhino):
var global = this;var x = 10;(function foo() {var y = 20;// "foo"上下文里的活動對象var AO = (function () {}).__parent__;print(AO.y); // 20// 當(dāng)前活動對象的__parent__ 是已經(jīng)存在的全局對象// 變量對象的特殊鏈形成了// 所以我們叫做作用域鏈print(AO.__parent__ === global); // trueprint(AO.__parent__.x); // 10})();
總結(jié)
在這篇文章里,我們深入學(xué)習(xí)了跟執(zhí)行上下文相關(guān)的對象。我希望這些知識對您來說能有所幫助,能解決一些您曾經(jīng)遇到的問題或困惑。

