感謝社區(qū)中各位的大力支持西土,譯者再次奉上一點(diǎn)點(diǎn)福利:阿里云產(chǎn)品券讶舰,享受所有官網(wǎng)優(yōu)惠,并抽取幸運(yùn)大獎(jiǎng):點(diǎn)擊這里領(lǐng)取
在第一章中需了,我們將“作用域”定義為一組規(guī)則跳昼,它主宰著 引擎 如何通過標(biāo)識符名稱在當(dāng)前的 作用域,或者在包含它的任意 嵌套作用域 中來查詢一個(gè)變量肋乍,
作用域的工作方式有兩種占統(tǒng)治地位的模型鹅颊。其中的第一種是最最常見,在絕大多數(shù)的編程語言中被使用的住拭。它稱為 詞法作用域挪略,我們將深入檢視它。另一種仍然被一些語言(比如Bash腳本滔岳,Perl中的一些模式杠娱,等等)使用的模型,稱為 動(dòng)態(tài)作用域谱煤。
動(dòng)態(tài)作用域在附錄A中講解摊求。我在這里提到它僅僅是為詞法作用域提供一個(gè)對比,而詞法作用域是JavaScript采用的作用域模型刘离。
詞法分析時(shí)
正如我們在第一章中討論的室叉,標(biāo)準(zhǔn)語言編譯器的第一個(gè)傳統(tǒng)步驟稱為詞法分析(也就是分詞)。如果你回憶一下硫惕,詞法分析處理是檢查一串源代碼字符茧痕,并給token賦予語法含義作為某種有狀態(tài)解析的輸出。
正是這個(gè)概念給理解詞法作用域是什么提供了基礎(chǔ)恼除,也是這個(gè)名詞的淵源踪旷。
要定義它有點(diǎn)兒兜圈子曼氛,詞法作用域是在詞法分析時(shí)被定義的作用域。換句話說令野,詞法作用域是基于舀患,你,在寫程序時(shí)气破,變量和作用域的塊兒在何處被編寫決定的聊浅,因此它在詞法分析器處理你的代碼時(shí)(基本上)是固定不變的。
注意: 我們將會稍稍看到有一些方法可以騙過詞法作用域现使,從而在詞法分析器處理過后改變它低匙,但是這些方法都是使人皺眉頭的。事實(shí)上公認(rèn)的最佳實(shí)踐是朴下,將詞法作用域看作是僅僅依靠詞法的努咐,因此在本質(zhì)上完全是編寫時(shí)決定的。
讓我們考慮這段代碼:
function foo(a) {
var b = a * 2;
function bar(c) {
console.log( a, b, c );
}
bar(b * 3);
}
foo( 2 ); // 2 4 12
在這個(gè)代碼實(shí)例中有三個(gè)固有的嵌套作用域殴胧。將這些作用域考慮為套在一起的氣泡可能有助于思考。
氣泡1 包圍著全局作用域佩迟,它里面只有一個(gè)標(biāo)識符:foo
团滥。
氣泡2 包圍著作用域foo
,它含有三個(gè)標(biāo)識符:a
报强,bar
和b
灸姊。
氣泡3 包圍著作用域bar
,它里面只包含一個(gè)標(biāo)識符:c
秉溉。
作用域氣泡是根據(jù)作用域的塊兒被寫在何處定義的力惯,一個(gè)嵌套在另一個(gè)內(nèi)部,等等召嘶。在下一章中父晶,我們將討論作用域的不同單位,但是就現(xiàn)在來說弄跌,讓我們認(rèn)為每一個(gè)函數(shù)創(chuàng)建了一個(gè)新的作用域氣泡甲喝。
bar
的氣泡完全被包含在foo
的氣泡中,因?yàn)椋ǘ抑灰驗(yàn)椋┻@就是我們選擇定義函數(shù)bar
的位置铛只。
注意這些嵌套的氣泡是嚴(yán)格嵌套的埠胖。我們沒有討論氣泡可以跨越邊界的維恩圖(Venn diagrams)。換句話說淳玩,沒有那個(gè)函數(shù)的氣泡可以同時(shí)(部分地)存在于另外兩個(gè)外部的作用域氣泡中直撤,就像沒有函數(shù)可以部分地存在于它的兩個(gè)父函數(shù)中一樣。
查詢
這些作用域氣泡的結(jié)構(gòu)和相對位置完全解釋了 引擎 在查找一個(gè)標(biāo)識符時(shí)蜕着,它需要查看的所有地方谋竖。
在上面的代碼段中,引擎 執(zhí)行語句console.log(..)
并開始查找三個(gè)被引用的變量a
,b
和c
圈盔。它首先從最內(nèi)部的作用域氣泡開始豹芯,也就是bar(..)
函數(shù)的作用域。在這里它找不到a
驱敲,所以它向上走一層铁蹈,到外面下一個(gè)最近的作用域氣泡,foo(..)
的作用域众眨。它在這里找到了a
握牧,于是它就使用這個(gè)a
。同樣的事情也發(fā)生在b
上娩梨。但是對于c
沿腰,它在bar(..)
內(nèi)部就找到了。
如果在bar(..)
內(nèi)部和foo(..)
內(nèi)部都有一個(gè)c
狈定,那么console.log(..)
語句將會找到并使用bar(..)
中的那一個(gè)颂龙,絕不會到達(dá)foo(..)
中的那一個(gè)。
一旦找到第一個(gè)匹配纽什,作用域查詢就停止了措嵌。相同的標(biāo)識符名稱可以在嵌套作用域的多個(gè)層中被指定,這稱為“遮蔽(shadowing)”(內(nèi)部的標(biāo)識符“遮蔽”了外部的標(biāo)識符)芦缰。無論如何遮蔽企巢,作用域查詢總是從當(dāng)前被執(zhí)行的最內(nèi)側(cè)的作用域開始,向外/向上不斷查找让蕾,直到第一個(gè)匹配才停止浪规。
注意: 全局變量也自動(dòng)地是全局對象(在瀏覽器中是window
,等等)的屬性探孝,所以不直接通過全局變量的詞法名稱笋婿,而通過將它作為全局對象的一個(gè)屬性引用來間接地引用,是可能的再姑。
window.a
這種技術(shù)給出了訪問全局變量的方法萌抵,沒有它全局變量將因?yàn)楸徽诒味豢稍L問。然而元镀,被遮蔽的非全局變量是無法訪問的绍填。
不管函數(shù)是從 哪里 被調(diào)用的,也不論它是 如何 被調(diào)用的栖疑,它的詞法作用域是由這個(gè)函數(shù)被聲明的位置 唯一 定義的讨永。
詞法作用域查詢 僅僅 在處理頭等標(biāo)識符時(shí)實(shí)施,比如a
遇革,b
卿闹,和c
揭糕。如果你在一段代碼中擁有一個(gè)foo.bar.baz
的引用,詞法作用域查詢將在查找foo
標(biāo)識符時(shí)實(shí)施锻霎,但一旦定位這個(gè)變量著角,對象屬性訪問規(guī)則將會分別接管bar
和baz
屬性的解析。
欺騙詞法作用域
如果詞法作用域僅僅是由函數(shù)被聲明的位置定義的旋恼,而且這個(gè)位置完全是一個(gè)編寫時(shí)的決定吏口,那么怎么可能有辦法在運(yùn)行時(shí)“修改”(也就是,作弊欺騙)詞法作用域呢冰更?
JavaScript有兩種這樣的機(jī)制产徊。在廣大的社區(qū)中它們都等同地被認(rèn)為是讓人皺眉頭的,在你代碼中使用它們是一種差勁兒的做法蜀细。但是關(guān)于它們的具有代表性的爭論經(jīng)常錯(cuò)過了最重要的一點(diǎn):欺騙詞法作用域會導(dǎo)致更低下的性能舟铜。
在我講解性能的問題以前,先讓我們看看這兩種機(jī)制是如何工作的奠衔。
eval
JavaScript中的eval(..)
函數(shù)接收一個(gè)字符串作為參數(shù)值谆刨,并將這個(gè)字符串的內(nèi)容看作是好像它已經(jīng)被實(shí)際編寫在程序的那個(gè)位置上。換句話說涣觉,你可以用編程的方式在你編寫好的代碼內(nèi)部生成代碼痴荐,而且你可以運(yùn)行這個(gè)生成的代碼,就好像它在編寫時(shí)就已經(jīng)在那里了一樣官册。
如果以這種觀點(diǎn)來評價(jià)eval(..)
,那么eval(..)
是如何允許你修改詞法作用域環(huán)境應(yīng)當(dāng)是很清楚的:欺騙并假裝這個(gè)編寫時(shí)(也就是难捌,詞法)代碼一直就在那里膝宁。
在eval(..)
被執(zhí)行的后續(xù)代碼行中,引擎 將不會“知道”或“關(guān)心”前面的代碼是被動(dòng)態(tài)翻譯的根吁,而且因此修改了詞法作用域環(huán)境员淫。引擎 將會像它一直做的那樣,簡單地進(jìn)行詞法作用域查詢击敌。
考慮如下代碼:
function foo(str, a) {
eval( str ); // 作弊介返!
console.log( a, b );
}
var b = 2;
foo( "var b = 3;", 1 ); // 1, 3
在eval(..)
調(diào)用的位置上,字符串"var b = 3"
被看作是一直就存在在那里的代碼沃斤。因?yàn)檫@個(gè)代碼恰巧聲明了一個(gè)新的變量b
圣蝎,它就修改了現(xiàn)存的foo(..)
的詞法作用域。事實(shí)上衡瓶,就像上面提到的那樣徘公,這個(gè)代碼實(shí)際上在foo(..)
內(nèi)部創(chuàng)建了變量b
,它遮蔽了聲明在外部(全局)作用域中的b
哮针。
當(dāng)console.log(..)
調(diào)用發(fā)生時(shí)关面,它會在foo(..)
的作用域中找到a
和b
坦袍,而且絕不會找到外部的b
。這樣等太,我們就打印出"1, 3"而不是一般情況下的"1, 2"捂齐。
注意: 在這個(gè)例子中,為了簡單起見缩抡,我們傳入的“代碼”字符串是固定的文字奠宜。但是它可以通過根據(jù)你的程序邏輯將字符拼接在一起,很容易地以編程方式創(chuàng)建缝其。eval(..)
通常被用于執(zhí)行動(dòng)態(tài)創(chuàng)建的代碼挎塌,因?yàn)閯?dòng)態(tài)地對一段實(shí)質(zhì)上源自字符串字面值的靜態(tài)代碼進(jìn)行求值,并不會比直接編寫這樣的代碼帶來更多真正的好處内边。
默認(rèn)情況下榴都,如果eval(..)
執(zhí)行的代碼字符串包含一個(gè)或多個(gè)聲明(變量或函數(shù))的話,這個(gè)動(dòng)作就會修改這個(gè)eval(..)
所在的詞法作用域漠其。技術(shù)上講嘴高,eval(..)
可以通過種種技巧(超出了我們這里的討論范圍)被“間接”調(diào)用,而使它在全局作用域的上下文中執(zhí)行和屎,如此修改全局作用域拴驮。但不論那種情況,eval(..)
都可以在運(yùn)行時(shí)修改一個(gè)編寫時(shí)的詞法作用域柴信。
注意: 當(dāng)eval(..)
被用于一個(gè)操作它自己的詞法作用域的strict模式程序時(shí)套啤,在eval(..)
內(nèi)部做出的聲明不會實(shí)際上修改包圍它的作用域。
function foo(str) {
"use strict";
eval( str );
console.log( a ); // ReferenceError: a is not defined
}
foo( "var a = 2" );
在JavaScript中還有其他的工具擁有與eval(..)
非常類似的效果随常。setTimeout(..)
和setInterval(..)
可以 為它們各自的第一個(gè)參數(shù)值接收一個(gè)字符串潜沦,其內(nèi)容將會被eval
為一個(gè)動(dòng)態(tài)生成的函數(shù)的代碼。這種老舊的绪氛,遺產(chǎn)行為早就被廢棄了唆鸡。別這么做!
new Function(..)
函數(shù)構(gòu)造器類似地為它的 最后 一個(gè)參數(shù)值接收一個(gè)代碼字符串枣察,來把它轉(zhuǎn)換為一個(gè)動(dòng)態(tài)生成的函數(shù)(前面的參數(shù)值争占,如果有的話,將作為新函數(shù)的命名參數(shù))序目。這種函數(shù)構(gòu)造器語法要比eval(..)
稍稍安全一些臂痕,但在你的代碼中它仍然應(yīng)當(dāng)被避免。
在你的代碼中動(dòng)態(tài)生成代碼的用例少的不可思議宛琅,因?yàn)樵谛阅苌系牡雇耸沟眠@種能力幾乎總是得不償失刻蟹。
with
JavaScript的另一個(gè)使人皺眉頭(而且現(xiàn)在被廢棄了!)嘿辟,而且可以欺騙詞法作用域的特性是with
關(guān)鍵字舆瘪。有許多種合法的方式可以講解with
片效,但是我在此選擇從它如何與詞法作用域互動(dòng)并影響詞法作用域的角度來講解它。
講解with
的典型方式是作為一種縮寫英古,來引用一個(gè)對象的多個(gè)屬性淀衣,而 不必 每次都重復(fù)對象引用本身忌傻。
例如:
var obj = {
a: 1,
b: 2,
c: 3
};
// 重復(fù)“obj”顯得更“繁冗”
obj.a = 2;
obj.b = 3;
obj.c = 4;
// “更簡單”的縮寫
with (obj) {
a = 3;
b = 4;
c = 5;
}
然而扔罪,這里發(fā)生的事情要比只是一個(gè)對象屬性訪問的便捷縮寫要多得多≌闾撸考慮如下代碼:
function foo(obj) {
with (obj) {
a = 2;
}
}
var o1 = {
a: 3
};
var o2 = {
b: 3
};
foo( o1 );
console.log( o1.a ); // 2
foo( o2 );
console.log( o2.a ); // undefined
console.log( a ); // 2 -- 哦唠叛,全局作用域被泄漏了只嚣!
在這個(gè)代碼示例中,創(chuàng)建了兩個(gè)對象o1
和o2
艺沼。一個(gè)有a
屬性册舞,而另一個(gè)沒有。foo(..)
函數(shù)接收一個(gè)對象引用obj
作為參數(shù)值障般,并在這個(gè)引用上調(diào)用with (obj) {..}
调鲸。在with
塊兒內(nèi)部,我們制造了一個(gè)變量a
的看似是普通詞法引用的東西挽荡,實(shí)際上是一個(gè)LHS引用(見第一章)藐石,并將值2
賦予它。
當(dāng)我們傳入o1
時(shí)定拟,賦值a = 2
找到屬性o1.a
并賦予它值2
于微,正如在后續(xù)的console.log(o1.a)
語句反應(yīng)的那樣。然而青自,當(dāng)我們傳入o2
角雷,因?yàn)樗鼪]有a
屬性,沒有這樣的屬性被創(chuàng)建性穿,所以o2.a
還是undefined
。
但是之后我們注意到一個(gè)特別的副作用雷滚,賦值a = 2
創(chuàng)建了一個(gè)全局變量a
需曾。這怎么可能?
with
語句接收一個(gè)對象祈远,這個(gè)對象有0個(gè)或多個(gè)屬性呆万,并 將這個(gè)對象視為好像它是一個(gè)完全隔離的詞法作用域,因此這個(gè)對象的屬性被視為在這個(gè)“作用域”中詞法定義的標(biāo)識符车份。
注意: 盡管一個(gè)with
塊兒將一個(gè)對象視為一個(gè)詞法作用域谋减,但是在with
塊兒內(nèi)部的一個(gè)普通var
聲明將不會歸于這個(gè)with
塊兒的作用域,而是歸于包含它的函數(shù)作用域扫沼。
如果eval(..)
函數(shù)接收一個(gè)含有一個(gè)或多個(gè)聲明的代碼字符串出爹,它就會修改現(xiàn)存的詞法作用域庄吼,而with
語句實(shí)際上是從你傳遞給它的對象中憑空制造了一個(gè) 全新的詞法作用域。
以這種方式理解的話严就,當(dāng)我們傳入o1
時(shí)with
語句聲明的“作用域”就是o1
总寻,而且這個(gè)“作用域”擁有一個(gè)對應(yīng)于o1.a
屬性的“標(biāo)識符”。但當(dāng)我們使用o2
作為“作用域”時(shí)梢为,它里面沒有這樣的a
“標(biāo)識符”渐行,于是LHS標(biāo)識符查詢(見第一章)的普通規(guī)則發(fā)生了。
“作用域”o2
中沒有铸董,foo(..)
的作用域中也沒有祟印,甚至連全局作作用域中都沒有找到標(biāo)識符a
,所以當(dāng)a = 2
被執(zhí)行時(shí)粟害,其結(jié)果就是自動(dòng)全局變量被創(chuàng)建(因?yàn)槲覀儧]有在strict模式下)蕴忆。
with
在運(yùn)行時(shí)將一個(gè)對象和它的屬性轉(zhuǎn)換為一個(gè)帶有“標(biāo)識符”的“作用域”,這個(gè)奇怪想法有些燒腦我磁。但是對于我們看到的結(jié)果來說孽文,這是我能給出的最清晰的解釋。
注意: 除了使用它們是個(gè)壞主意意外夺艰,eval(..)
和with
都受Strict模式的影響(制約)芋哭。with
干脆就不允許使用,而雖然eval(..)
還保有其核心功能郁副,但各種間接形式的或不安全的eval(..)
是不允許的减牺。
性能
通過在運(yùn)行時(shí)修改,或創(chuàng)建新的詞法作用域存谎,eval(..)
和with
都可以欺騙編寫時(shí)定義的詞法作用域拔疚。
你可能會問,那又有什么大不了的既荚?如果它們提供了更精巧的功能和編碼靈活性稚失,那它們不是 好的 特性嗎?不恰聘。
JavaScript 引擎 在編譯階段期行許多性能優(yōu)化工作句各。其中的一些優(yōu)化原理都?xì)w結(jié)為實(shí)質(zhì)上在進(jìn)行詞法分析時(shí)可以靜態(tài)地分析代碼,并提前決定所有的變量和函數(shù)聲明都在什么位置晴叨,這樣在執(zhí)行期間就可以少花些力氣來解析標(biāo)識符凿宾。
但如果 引擎 在代碼中找到一個(gè)eval(..)
或with
,它實(shí)質(zhì)上就不得不 假定 自己知道的所有的標(biāo)識符的位置可能是不合法的兼蕊,因?yàn)樗豢赡茉谠~法分析時(shí)就知道你將會向eval(..)
傳遞什么樣的代碼來修改詞法作用域初厚,或者你可能會向with
傳遞的對象有什么樣的內(nèi)容來創(chuàng)建一個(gè)新的將被查詢的詞法作用域。
換句話說孙技,悲觀地看产禾,如果eval(..)
或with
出現(xiàn)排作,那么它 將 做的幾乎所有的優(yōu)化都會變得沒有意義,所以它就會簡單地根本不做任何優(yōu)化下愈。
你的代碼幾乎肯定會趨于運(yùn)行的更慢纽绍,只因?yàn)槟阍诖a的任何地方引入了一個(gè)了eval(..)
或with
。無論 引擎 將在努力限制這些悲觀臆測的副作用上表現(xiàn)得多么聰明势似,都沒有任何辦法可以繞過這個(gè)事實(shí):沒有優(yōu)化拌夏,代碼就運(yùn)行的更慢。
復(fù)習(xí)
詞法作用域意味著作用域是由編寫時(shí)函數(shù)被聲明的位置的決策定義的履因。編譯器的詞法分析階段實(shí)質(zhì)上可以知道所有的標(biāo)識符是在哪里和如何聲明的障簿,并如此在執(zhí)行期間預(yù)測它們將如何被查詢。
在JavaScript中有兩種機(jī)制可以“欺騙”詞法作用域:eval(..)
和with
栅迄。前者可以通過對一個(gè)擁有一個(gè)或多個(gè)聲明的“代碼”字符串進(jìn)行求值站故,來(在運(yùn)行時(shí))修改現(xiàn)存的詞法作用域。后者實(shí)質(zhì)上是通過將一個(gè)對象引用看作一個(gè)“作用域”毅舆,并將這個(gè)對象的屬性看作作用域中的標(biāo)識符西篓,(同樣,也是在運(yùn)行時(shí))創(chuàng)建一個(gè)全新的詞法作用域憋活。
這些機(jī)制的缺點(diǎn)是岂津,它壓制了 引擎 在作用域查詢上進(jìn)行編譯期優(yōu)化的能力,因?yàn)?引擎 不得不悲觀地假定這樣的優(yōu)化是不合法的悦即。這兩種特性的結(jié)果就是代碼 將 會運(yùn)行的更慢吮成。不要使用它們。