在第12章關(guān)于變量對(duì)象的描述中,我們已經(jīng)知道一個(gè)執(zhí)行上下文 的數(shù)據(jù)(變量、函數(shù)聲明和函數(shù)的形參)作為屬性存儲(chǔ)在變量對(duì)象中。
同時(shí)我們也知道變量對(duì)象在每次進(jìn)入上下文時(shí)創(chuàng)建,并填入初始值,值的更新出現(xiàn)在代碼執(zhí)行階段。
這一章專門討論與執(zhí)行上下文直接相關(guān)的更多細(xì)節(jié),這次我們將提及一個(gè)議題——作用域鏈。
英文原文:http://dmitrysoshnikov.com/ecmascript/chapter-4-scope-chain/ 中文參考:http://www.denisdeng.com/?p=908 本文絕大部分內(nèi)容來自上述地址,僅做少許修改,感謝作者
如果要簡要的描述并展示其重點(diǎn),那么作用域鏈大多數(shù)與內(nèi)部函數(shù)相關(guān)。
我們知道,ECMAScript 允許創(chuàng)建內(nèi)部函數(shù),我們甚至能從父函數(shù)中返回這些函數(shù)。
var x = 10; function foo() { var y = 20; function bar() { alert(x + y); } return bar; } foo()(); // 30
這樣,很明顯每個(gè)上下文擁有自己的變量對(duì)象:對(duì)于全局上下文,它是全局對(duì)象自身;對(duì)于函數(shù),它是活動(dòng)對(duì)象。
作用域鏈正是內(nèi)部上下文所有變量對(duì)象(包括父變量對(duì)象)的列表。此鏈用來變量查詢。即在上面的例子中,“bar”上下文的作用域鏈包括AO(bar)、AO(foo)和VO(global)。
但是,讓我們仔細(xì)研究這個(gè)問題。
讓我們從定義開始,并進(jìn)深一步的討論示例。
作用域鏈與一個(gè)執(zhí)行上下文相關(guān),變量對(duì)象的鏈用于在標(biāo)識(shí)符解析中變量查找。
函數(shù)上下文的作用域鏈在函數(shù)調(diào)用時(shí)創(chuàng)建的,包含活動(dòng)對(duì)象和這個(gè)函數(shù)內(nèi)部的[[scope]]屬性。下面我們將更詳細(xì)的討論一個(gè)函數(shù)的[[scope]]屬性。
在上下文中示意如下:
activeExecutionContext = { VO: {...}, // or AO this: thisValue, Scope: [ // Scope chain // 所有變量對(duì)象的列表 // for identifiers lookup ] };
其scope定義如下:
Scope = AO + [[Scope]]
這種聯(lián)合和標(biāo)識(shí)符解析過程,我們將在下面討論,這與函數(shù)的生命周期相關(guān)。
函數(shù)的的生命周期分為創(chuàng)建和激活階段(調(diào)用時(shí)),讓我們詳細(xì)研究它。
眾所周知,在進(jìn)入上下文時(shí)函數(shù)聲明放到變量/活動(dòng)(VO/AO)對(duì)象中。讓我們看看在全局上下文中的變量和函數(shù)聲明(這里變量對(duì)象是全局對(duì)象自身,我們還記得,是吧?)
var x = 10; function foo() { var y = 20; alert(x + y); } foo(); // 30
在函數(shù)激活時(shí),我們得到正確的(預(yù)期的)結(jié)果--30。但是,有一個(gè)很重要的特點(diǎn)。
此前,我們僅僅談到有關(guān)當(dāng)前上下文的變量對(duì)象。這里,我們看到變量“y”在函數(shù)“foo”中定義(意味著它在foo上下文的AO中),但是變量“x”并未在“foo”上下文中定義,相應(yīng)地,它也不會(huì)添加到“foo”的AO中。乍一看,變量“x”相對(duì)于函數(shù)“foo”根本就不存在;但正如我們在下面看到的——也僅僅是“一瞥”,我們發(fā)現(xiàn),“foo”上下文的活動(dòng)對(duì)象中僅包含一個(gè)屬性--“y”。
fooContext.AO = { y: undefined // undefined – 進(jìn)入上下文的時(shí)候是20 – at activation };
函數(shù)“foo”如何訪問到變量“x”?理論上函數(shù)應(yīng)該能訪問一個(gè)更高一層上下文的變量對(duì)象。實(shí)際上它正是這樣,這種機(jī)制是通過函數(shù)內(nèi)部的[[scope]]屬性來實(shí)現(xiàn)的。
[[scope]]是所有父變量對(duì)象的層級(jí)鏈,處于當(dāng)前函數(shù)上下文之上,在函數(shù)創(chuàng)建時(shí)存于其中。
注意這重要的一點(diǎn)--[[scope]]在函數(shù)創(chuàng)建時(shí)被存儲(chǔ)--靜態(tài)(不變的),永遠(yuǎn)永遠(yuǎn),直至函數(shù)銷毀。即:函數(shù)可以永不調(diào)用,但[[scope]]屬性已經(jīng)寫入,并存儲(chǔ)在函數(shù)對(duì)象中。
另外一個(gè)需要考慮的是--與作用域鏈對(duì)比,[[scope]]是函數(shù)的一個(gè)屬性而不是上下文??紤]到上面的例子,函數(shù)“foo”的[[scope]]如下:
foo.[[Scope]] = [ globalContext.VO // === Global ];
舉例來說,我們用通常的ECMAScript 數(shù)組展現(xiàn)作用域和[[scope]]。
繼續(xù),我們知道在函數(shù)調(diào)用時(shí)進(jìn)入上下文,這時(shí)候活動(dòng)對(duì)象被創(chuàng)建,this和作用域(作用域鏈)被確定。讓我們詳細(xì)考慮這一時(shí)刻。
正如在定義中說到的,進(jìn)入上下文創(chuàng)建AO/VO之后,上下文的Scope屬性(變量查找的一個(gè)作用域鏈)作如下定義:
Scope = AO|VO + [[Scope]]
上面代碼的意思是:活動(dòng)對(duì)象是作用域數(shù)組的第一個(gè)對(duì)象,即添加到作用域的前端。
Scope = [AO].concat([[Scope]]);
這個(gè)特點(diǎn)對(duì)于標(biāo)示符解析的處理來說很重要。
標(biāo)示符解析是一個(gè)處理過程,用來確定一個(gè)變量(或函數(shù)聲明)屬于哪個(gè)變量對(duì)象。
這個(gè)算法的返回值中,我們總有一個(gè)引用類型,它的base組件是相應(yīng)的變量對(duì)象(或若未找到則為null),屬性名組件是向上查找的標(biāo)示符的名稱。引用類型的詳細(xì)信息在第13章.this中已討論。
標(biāo)識(shí)符解析過程包含與變量名對(duì)應(yīng)屬性的查找,即作用域中變量對(duì)象的連續(xù)查找,從最深的上下文開始,繞過作用域鏈直到最上層。
這樣一來,在向上查找中,一個(gè)上下文中的局部變量較之于父作用域的變量擁有較高的優(yōu)先級(jí)。萬一兩個(gè)變量有相同的名稱但來自不同的作用域,那么第一個(gè)被發(fā)現(xiàn)的是在最深作用域中。
我們用一個(gè)稍微復(fù)雜的例子描述上面講到的這些。
var x = 10; function foo() { var y = 20; function bar() { var z = 30; alert(x + y + z); } bar(); } foo(); // 60
對(duì)此,我們有如下的變量/活動(dòng)對(duì)象,函數(shù)的的[[scope]]屬性以及上下文的作用域鏈:
全局上下文的變量對(duì)象是:
globalContext.VO === Global = { x: 10 foo: <reference to function> };
在“foo”創(chuàng)建時(shí),“foo”的[[scope]]屬性是:
foo.[[Scope]] = [ globalContext.VO ];
在“foo”激活時(shí)(進(jìn)入上下文),“foo”上下文的活動(dòng)對(duì)象是:
fooContext.AO = { y: 20, bar: <reference to function> };
“foo”上下文的作用域鏈為:
fooContext.Scope = fooContext.AO + foo.[[Scope]] // i.e.: fooContext.Scope = [ fooContext.AO, globalContext.VO ];
內(nèi)部函數(shù)“bar”創(chuàng)建時(shí),其[[scope]]為:
bar.[[Scope]] = [ fooContext.AO, globalContext.VO ];
在“bar”激活時(shí),“bar”上下文的活動(dòng)對(duì)象為:
barContext.AO = { z: 30 };
“bar”上下文的作用域鏈為:
barContext.Scope = barContext.AO + bar.[[Scope]] // i.e.: barContext.Scope = [ barContext.AO, fooContext.AO, globalContext.VO ];
對(duì)“x”、“y”、“z”的標(biāo)識(shí)符解析如下:
- "x" -- barContext.AO // not found -- fooContext.AO // not found -- globalContext.VO // found - 10 - "y" -- barContext.AO // not found -- fooContext.AO // found - 20 - "z" -- barContext.AO // found - 30
讓我們看看與作用域鏈和函數(shù)[[scope]]屬性相關(guān)的一些重要特征。
在ECMAScript中,閉包與函數(shù)的[[scope]]直接相關(guān),正如我們提到的那樣,[[scope]]在函數(shù)創(chuàng)建時(shí)被存儲(chǔ),與函數(shù)共存亡。實(shí)際上,閉包是函數(shù)代碼和其[[scope]]的結(jié)合。因此,作為其對(duì)象之一,[[Scope]]包括在函數(shù)內(nèi)創(chuàng)建的詞法作用域(父變量對(duì)象)。當(dāng)函數(shù)進(jìn)一步激活時(shí),在變量對(duì)象的這個(gè)詞法鏈(靜態(tài)的存儲(chǔ)于創(chuàng)建時(shí))中,來自較高作用域的變量將被搜尋。
例如:
var x = 10; function foo() { alert(x); } (function () { var x = 20; foo(); // 10, but not 20 })();
我們再次看到,在標(biāo)識(shí)符解析過程中,使用函數(shù)創(chuàng)建時(shí)定義的詞法作用域--變量解析為10,而不是30。此外,這個(gè)例子也清晰的表明,一個(gè)函數(shù)(這個(gè)例子中為從函數(shù)“foo”返回的匿名函數(shù))的[[scope]]持續(xù)存在,即使是在函數(shù)創(chuàng)建的作用域已經(jīng)完成之后。
關(guān)于ECMAScript中閉包的理論和其執(zhí)行機(jī)制的更多細(xì)節(jié),閱讀16章閉包。
在上面的例子中,我們看到,在函數(shù)創(chuàng)建時(shí)獲得函數(shù)的[[scope]]屬性,通過該屬性訪問到所有父上下文的變量。但是,這個(gè)規(guī)則有一個(gè)重要的例外,它涉及到通過函數(shù)構(gòu)造函數(shù)創(chuàng)建的函數(shù)。
var x = 10; function foo() { var y = 20; function barFD() { // 函數(shù)聲明 alert(x); alert(y); } var barFE = function () { // 函數(shù)表達(dá)式 alert(x); alert(y); }; var barFn = Function('alert(x); alert(y);'); barFD(); // 10, 20 barFE(); // 10, 20 barFn(); // 10, "y" is not defined } foo();
我們看到,通過函數(shù)構(gòu)造函數(shù)(Function constructor)創(chuàng)建的函數(shù)“bar”,是不能訪問變量“y”的。但這并不意味著函數(shù)“barFn”沒有[[scope]]屬性(否則它不能訪問到變量“x”)。問題在于通過函構(gòu)造函數(shù)創(chuàng)建的函數(shù)的[[scope]]屬性總是唯一的全局對(duì)象??紤]到這一點(diǎn),如通過這種函數(shù)創(chuàng)建除全局之外的最上層的上下文閉包是不可能的。
在作用域鏈中查找最重要的一點(diǎn)是變量對(duì)象的屬性(如果有的話)須考慮其中--源于ECMAScript 的原型特性。如果一個(gè)屬性在對(duì)象中沒有直接找到,查詢將在原型鏈中繼續(xù)。即常說的二維鏈查找。(1)作用域鏈環(huán)節(jié);(2)每個(gè)作用域鏈--深入到原型鏈環(huán)節(jié)。如果在Object.prototype 中定義了屬性,我們能看到這種效果。
function foo() { alert(x); } Object.prototype.x = 10; foo(); // 10
活動(dòng)對(duì)象沒有原型,我們可以在下面的例子中看到:
function foo() { var x = 20; function bar() { alert(x); } bar(); } Object.prototype.x = 10; foo(); // 20
如果函數(shù)“bar”上下文的激活對(duì)象有一個(gè)原型,那么“x”將在Object.prototype 中被解析,因?yàn)樗贏O中不被直接解析。但在上面的第一個(gè)例子中,在標(biāo)識(shí)符解析中,我們到達(dá)全局對(duì)象(在一些執(zhí)行中并不全是這樣),它從Object.prototype繼承而來,響應(yīng)地,“x”解析為10。
同樣的情況出現(xiàn)在一些版本的SpiderMokey 的命名函數(shù)表達(dá)式(縮寫為NFE)中,在那里特定的對(duì)象存儲(chǔ)從Object.prototype繼承而來的函數(shù)表達(dá)式的可選名稱,在Blackberry中的一些版本中,執(zhí)行時(shí)激活對(duì)象從Object.prototype繼承。但是,關(guān)于該特色的更多細(xì)節(jié)在第15章函數(shù)討論。
這里不一定很有趣,但必須要提示一下。全局上下文的作用域鏈僅包含全局對(duì)象。代碼eval的上下文與當(dāng)前的調(diào)用上下文(calling context)擁有同樣的作用域鏈。
globalContext.Scope = [ Global ]; evalContext.Scope === callingContext.Scope;
在ECMAScript 中,在代碼執(zhí)行階段有兩個(gè)聲明能修改作用域鏈。這就是with聲明和catch語句。它們添加到作用域鏈的最前端,對(duì)象須在這些聲明中出現(xiàn)的標(biāo)識(shí)符中查找。如果發(fā)生其中的一個(gè),作用域鏈簡要的作如下修改:
Scope = withObject|catchObject + AO|VO + [[Scope]]
在這個(gè)例子中添加對(duì)象,對(duì)象是它的參數(shù)(這樣,沒有前綴,這個(gè)對(duì)象的屬性變得可以訪問)。
var foo = {x: 10, y: 20}; with (foo) { alert(x); // 10 alert(y); // 20 }
作用域鏈修改成這樣:
Scope = foo + AO|VO + [[Scope]]
我們再次看到,通過with語句,對(duì)象中標(biāo)識(shí)符的解析添加到作用域鏈的最前端:
var x = 10, y = 10; with ({x: 20}) { var x = 30, y = 30; alert(x); // 30 alert(y); // 30 } alert(x); // 10 alert(y); // 30
在進(jìn)入上下文時(shí)發(fā)生了什么?標(biāo)識(shí)符“x”和“y”已被添加到變量對(duì)象中。此外,在代碼運(yùn)行階段作如下修改:
同樣,catch語句的異常參數(shù)變得可以訪問,它創(chuàng)建了只有一個(gè)屬性的新對(duì)象--異常參數(shù)名。圖示看起來像這樣:
try { ... } catch (ex) { alert(ex); }
作用域鏈修改為:
var catchObject = { ex: <exception object> }; Scope = catchObject + AO|VO + [[Scope]]
在catch語句完成運(yùn)行之后,作用域鏈恢復(fù)到以前的狀態(tài)。
在這個(gè)階段,我們幾乎考慮了與執(zhí)行上下文相關(guān)的所有常用概念,以及與它們相關(guān)的細(xì)節(jié)。按照計(jì)劃--函數(shù)對(duì)象的詳細(xì)分析:函數(shù)類型(函數(shù)聲明,函數(shù)表達(dá)式)和閉包。順便說一下,在這篇文章中,閉包直接與[[scope]]屬性相關(guān),但是,關(guān)于它將在合適的篇章中討論。我很樂意在評(píng)論中回答你的問題。
更多建議: