你不懂JS:作用域與閉包
第一章:什么是作用域?
幾乎所有語(yǔ)言的最基礎(chǔ)模型之一就是在變量中存儲(chǔ)值廊驼,并且在稍后取出或修改這些值的能力据过。事實(shí)上惋砂,在變量中存儲(chǔ)值和取出值的能力,給程序賦予了 狀態(tài)绳锅。
如果沒(méi)有這樣的概念西饵,一個(gè)程序雖然可以執(zhí)行一些任務(wù),但是它們將會(huì)受到極大的限制而且不會(huì)非常有趣榨呆。
但是在我們的程序中納入變量罗标,引出了我們現(xiàn)在將要解決的最有趣的問(wèn)題:這些變量 存活 在哪里?換句話(huà)說(shuō)积蜻,它們被存儲(chǔ)在哪兒闯割?而且,最重要的是竿拆,我們的程序如何在需要它們的時(shí)候找到它們宙拉?
回答這些問(wèn)題需要一組明確定義的規(guī)則,它定義如何在某些位置存儲(chǔ)變量丙笋,以及如何在稍后找到這些變量谢澈。我們稱(chēng)這組規(guī)則為:作用域。
但是御板,這些 作用域 規(guī)則是在哪里锥忿、如何被設(shè)置的?
編譯器理論
根據(jù)你與各種編程語(yǔ)言打交道的水平不同怠肋,這也許是不證自明的敬鬓,或者這也許令人吃驚,盡管 JavaScript 一般被劃分到“動(dòng)態(tài)”或者“解釋型”語(yǔ)言的范疇笙各,但是其實(shí)它是一個(gè)編譯型語(yǔ)言钉答。它 不是 像許多傳統(tǒng)意義上的編譯型語(yǔ)言那樣預(yù)先被編譯好,編譯的結(jié)果也不能在各種不同的分布式系統(tǒng)間移植杈抢。
但是無(wú)論如何数尿,JavaScript 引擎在實(shí)施許多與傳統(tǒng)的語(yǔ)言編譯器相同的步驟,雖然是以一種我們不易察覺(jué)的更精巧的方式惶楼。
在傳統(tǒng)的編譯型語(yǔ)言處理中右蹦,一塊兒源代碼,你的程序歼捐,在它被執(zhí)行 之前 通常將會(huì)經(jīng)歷三個(gè)步驟何陆,大致被稱(chēng)為“編譯”:
-
分詞/詞法分析: 將一連串字符打斷成(對(duì)于語(yǔ)言來(lái)說(shuō))有意義的片段,稱(chēng)為 token(記號(hào))窥岩。舉例來(lái)說(shuō),考慮這段程序:
var a = 2;
宰缤。這段程序很可能會(huì)被打斷成如下 token:var
颂翼,a
晃洒,=
,2
朦乏,和;
球及。空格也許會(huì)被保留為一個(gè) token呻疹,這要看它是否是有意義的吃引。注意: 分詞和詞法分析之間的區(qū)別是微妙和學(xué)術(shù)上的,其中心在于這些 token 是否以 無(wú)狀態(tài) 或 有狀態(tài) 的方式被識(shí)別刽锤。簡(jiǎn)而言之镊尺,如果分詞器去調(diào)用有狀態(tài)的解析規(guī)則來(lái)弄清
a
是否應(yīng)當(dāng)被考慮為一個(gè)不同的 token,還是只是其他 token 的一部分并思,那么這就是 詞法分析庐氮。 -
解析: 將一個(gè) token 的流(數(shù)組)轉(zhuǎn)換為一個(gè)嵌套元素的樹(shù),它綜合地表示了程序的語(yǔ)法結(jié)構(gòu)。這棵樹(shù)稱(chēng)為“抽象語(yǔ)法樹(shù)”(AST —— <b>A</b>bstract <b>S</b>yntax <b>T</b>ree)。
var a = 2;
的樹(shù)也許開(kāi)始于稱(chēng)為VariableDeclaration
(變量聲明)頂層節(jié)點(diǎn)羹与,帶有一個(gè)稱(chēng)為Identifier
(標(biāo)識(shí)符)的子節(jié)點(diǎn)(它的值為a
)账胧,和另一個(gè)稱(chēng)為AssignmentExpression
(賦值表達(dá)式)的子節(jié)點(diǎn),而這個(gè)子節(jié)點(diǎn)本身帶有一個(gè)稱(chēng)為NumericLiteral
(數(shù)字字面量)的子節(jié)點(diǎn)(它的值為2
)赔硫。 -
代碼生成: 這個(gè)處理將抽象語(yǔ)法樹(shù)轉(zhuǎn)換為可執(zhí)行的代碼。這一部分將根據(jù)語(yǔ)言,它的目標(biāo)平臺(tái)等因素有很大的不同衣式。
所以,與其深陷細(xì)節(jié)型奥,我們不如籠統(tǒng)地說(shuō)瞳收,有一種方法將我們上面描述的
var a = 2;
的抽象語(yǔ)法樹(shù)轉(zhuǎn)換為機(jī)器指令,來(lái)實(shí)際上 創(chuàng)建 一個(gè)稱(chēng)為a
的變量(包括分配內(nèi)存等等)厢汹,然后在a
中存入一個(gè)值螟深。注意: 引擎如何管理系統(tǒng)資源的細(xì)節(jié)遠(yuǎn)比我們要挖掘的東西深刻,所以我們將理所當(dāng)然地認(rèn)為引擎有能力按其需要?jiǎng)?chuàng)建和存儲(chǔ)變量烫葬。
和大多數(shù)其他語(yǔ)言的編譯器一樣界弧,JavaScript 引擎要比這區(qū)區(qū)三步復(fù)雜太多了。例如搭综,在解析和代碼生成的處理中垢箕,一定會(huì)存在優(yōu)化執(zhí)行效率的步驟,包括壓縮冗余元素兑巾,等等条获。
所以,我在此描繪的只是大框架蒋歌。但是我想你很快就會(huì)明白為什么我們涵蓋的這些細(xì)節(jié)是重要的帅掘,雖然是在很高的層次上委煤。
其一,JavaScript 引擎沒(méi)有(像其他語(yǔ)言的編譯器那樣)大把的時(shí)間去優(yōu)化修档,因?yàn)?JavaScript 的編譯和其他語(yǔ)言不同碧绞,不是提前發(fā)生在一個(gè)構(gòu)建的步驟中。
對(duì) JavaScript 來(lái)說(shuō)吱窝,在許多情況下讥邻,編譯發(fā)生在代碼被執(zhí)行前的僅僅幾微秒之內(nèi)(或更少!)院峡。為了確保最快的性能兴使,JS 引擎將使用所有的招數(shù)(比如 JIT,它可以懶編譯甚至是熱編譯撕予,等等)鲫惶,而這遠(yuǎn)超出了我們關(guān)于“作用域”的討論。
為了簡(jiǎn)單起見(jiàn)实抡,我們可以說(shuō)欠母,任何 JavaScript 代碼段在它執(zhí)行之前(通常是 剛好 在它執(zhí)行之前!)都必須被編譯吆寨。所以赏淌,JS 編譯器將把程序 var a = 2;
拿過(guò)來(lái),并首先編譯它啄清,然后準(zhǔn)備運(yùn)行它六水,通常是立即的。
理解作用域
我們將采用的學(xué)習(xí)作用域的方法辣卒,是將這個(gè)處理過(guò)程想象為一場(chǎng)對(duì)話(huà)掷贾。但是,誰(shuí) 在進(jìn)行這場(chǎng)對(duì)話(huà)呢荣茫?
演員
讓我們見(jiàn)一見(jiàn)處理程序 var a = 2;
時(shí)進(jìn)行互動(dòng)的演員吧想帅,這樣我們就能理解稍后將要聽(tīng)到的它們的對(duì)話(huà):
引擎:負(fù)責(zé)從始至終的編譯和執(zhí)行我們的 JavaScript 程序。
編譯器:引擎 的朋友之一啡莉;處理所有的解析和代碼生成的重活兒(見(jiàn)前一節(jié))港准。
作用域:引擎 的另一個(gè)朋友;收集并維護(hù)一張所有被聲明的標(biāo)識(shí)符(變量)的列表咧欣,并對(duì)當(dāng)前執(zhí)行中的代碼如何訪(fǎng)問(wèn)這些變量強(qiáng)制實(shí)施一組嚴(yán)格的規(guī)則浅缸。
為了 全面理解 JavaScript 是如何工作的,你需要開(kāi)始像 引擎(和它的朋友們)那樣 思考魄咕,問(wèn)它們問(wèn)的問(wèn)題衩椒,并像它們一樣回答。
反復(fù)
當(dāng)你看到程序 var a = 2;
時(shí),你很可能認(rèn)為它是一個(gè)語(yǔ)句毛萌。但這不是我們的新朋友 引擎 所看到的梢什。事實(shí)上,引擎 看到兩個(gè)不同的語(yǔ)句朝聋,一個(gè)是 編譯器 將在編譯期間處理的,一個(gè)是 引擎 將在執(zhí)行期間處理的囤躁。
那么冀痕,讓我們來(lái)分析 引擎 和它的朋友們將如何處理程序 var a = 2;
。
編譯器 將對(duì)這個(gè)程序做的第一件事情狸演,是進(jìn)行詞法分析來(lái)將它分解為一系列 token言蛇,然后這些 token 被解析為一棵樹(shù)。但是當(dāng) 編譯器 到了代碼生成階段時(shí)宵距,它會(huì)以一種與我們可能想象的不同的方式來(lái)對(duì)待這段程序腊尚。
一個(gè)合理的假設(shè)是,編譯器 將產(chǎn)生的代碼可以用這種假想代碼概括:“為一個(gè)變量分配內(nèi)存满哪,將它標(biāo)記為 a
婿斥,然后將值 2
貼在這個(gè)變量里”。不幸的是哨鸭,這不是十分準(zhǔn)確民宿。
編譯器 將會(huì)這樣處理:
遇到
var a
,編譯器 讓 作用域 去查看對(duì)于這個(gè)特定的作用域集合像鸡,變量a
是否已經(jīng)存在了活鹰。如果是,編譯器 就忽略這個(gè)聲明并繼續(xù)前進(jìn)只估。否則志群,編譯器 就讓 作用域 去為這個(gè)作用域集合聲明一個(gè)稱(chēng)為a
的新變量。然后 編譯器 為 引擎 生成稍后要執(zhí)行的代碼蛔钙,來(lái)處理賦值
a = 2
锌云。引擎 運(yùn)行的代碼首先讓 作用域 去查看在當(dāng)前的作用域集合中是否有一個(gè)稱(chēng)為a
的變量可以訪(fǎng)問(wèn)。如果有夸楣,引擎 就使用這個(gè)變量宾抓。如果沒(méi)有,引擎 就查看 其他地方(參見(jiàn)下面的嵌套 作用域 一節(jié))豫喧。
如果 引擎 最終找到一個(gè)變量石洗,它就將值 2
賦予它。如果沒(méi)有紧显,引擎 將會(huì)舉起它的手并喊出一個(gè)錯(cuò)誤讲衫!
總結(jié)來(lái)說(shuō):對(duì)于一個(gè)變量賦值,發(fā)生了兩個(gè)不同的動(dòng)作:第一,編譯器 聲明一個(gè)變量(如果先前沒(méi)有在當(dāng)前作用域中聲明過(guò))涉兽,第二招驴,當(dāng)執(zhí)行時(shí),引擎 在 作用域 中查詢(xún)這個(gè)變量并給它賦值枷畏,如果找到的話(huà)别厘。
編譯器術(shù)語(yǔ)
為了繼續(xù)更深入地理解,我們需要一點(diǎn)兒更多的編譯器術(shù)語(yǔ)拥诡。
當(dāng) 引擎 執(zhí)行 編譯器 在第二步為它產(chǎn)生的代碼時(shí)触趴,它必須查詢(xún)變量 a
來(lái)看它是否已經(jīng)被聲明過(guò)了,而且這個(gè)查詢(xún)是咨詢(xún) 作用域 的渴肉。但是 引擎 所實(shí)施的查詢(xún)的類(lèi)型會(huì)影響查詢(xún)的結(jié)果冗懦。
在我們這個(gè)例子中,引擎 將會(huì)對(duì)變量 a
實(shí)施一個(gè)“LHS”查詢(xún)仇祭。另一種類(lèi)型的查詢(xún)稱(chēng)為“RHS”披蕉。
我打賭你能猜出“L”和“R”是什么意思。這兩個(gè)術(shù)語(yǔ)表示“Left-hand Side(左手邊)”和“Right-hand Side(右手邊)”
什么的……邊乌奇?賦值操作的没讲。
換言之,當(dāng)一個(gè)變量出現(xiàn)在賦值操作的左手邊時(shí)礁苗,會(huì)進(jìn)行 LHS 查詢(xún)食零,當(dāng)一個(gè)變量出現(xiàn)在賦值操作的右手邊時(shí),會(huì)進(jìn)行 RHS 查詢(xún)寂屏。
實(shí)際上贰谣,我們可以表述得更準(zhǔn)確一點(diǎn)兒。對(duì)于我們的目的來(lái)說(shuō)迁霎,一個(gè) RHS 是難以察覺(jué)的吱抚,因?yàn)樗?jiǎn)單地查詢(xún)某個(gè)變量的值,而 LHS 查詢(xún)是試著找到變量容器本身考廉,以便它可以賦值秘豹。從這種意義上說(shuō),RHS 的含義實(shí)質(zhì)上不是 真正的 “一個(gè)賦值的右手邊”昌粤,更準(zhǔn)確地說(shuō)既绕,它只是意味著“不是左手邊”。
在這一番油腔滑調(diào)之后涮坐,你也可以認(rèn)為“RHS”意味著“取得他/她的源(值)”凄贩,暗示著 RHS 的意思是“去取……的值”。
讓我們挖掘得更深一些袱讹。
當(dāng)我說(shuō):
console.log( a );
這個(gè)指向 a
的引用是一個(gè) RHS 引用疲扎,因?yàn)檫@里沒(méi)有東西被賦值給 a
。而是我們?cè)诓樵?xún) a
并取得它的值,這樣這個(gè)值可以被傳遞進(jìn) console.log(..)
椒丧。
作為對(duì)比:
a = 2;
這里指向 a
的引用是一個(gè) LHS 引用壹甥,因?yàn)槲覀儗?shí)際上不關(guān)心當(dāng)前的值是什么,我們只是想找到這個(gè)變量壶熏,將它作為 = 2
賦值操作的目標(biāo)句柠。
注意: LHS 和 RHS 意味著“賦值的左/右手邊”未必像字面上那樣意味著“ =
賦值操作符的左/右邊”。賦值有幾種其他的發(fā)生形式棒假,所以最好在概念上將它考慮為:“賦值的目標(biāo)(LHS)”和“賦值的源(RHS)”俄占。
考慮這段程序,它既有 LHS 引用又有 RHS 引用:
function foo(a) {
console.log( a ); // 2
}
foo( 2 );
調(diào)用 foo(..)
的最后一行作為一個(gè)函數(shù)調(diào)用要求一個(gè)指向 foo
的 RHS 引用淆衷,意味著,“去查詢(xún) foo
的值渤弛,并把它交給我”祝拯。另外,(..)
意味著 foo
的值應(yīng)當(dāng)被執(zhí)行她肯,所以它最好實(shí)際上是一個(gè)函數(shù)佳头!
這里有一個(gè)微妙但重要的賦值。你發(fā)現(xiàn)了嗎晴氨?
你可能錯(cuò)過(guò)了這個(gè)代碼段隱含的 a = 2
康嘉。它發(fā)生在當(dāng)值 2
作為參數(shù)值傳遞給 foo(..)
函數(shù)時(shí),值 2
被賦值 給了參數(shù) a
籽前。為了(隱含地)給參數(shù) a
賦值亭珍,進(jìn)行了一個(gè) LHS 查詢(xún)。
這里還有一個(gè) a
的值的 RHS 引用枝哄,它的結(jié)果值被傳入 console.log(..)
肄梨。console.log(..)
需要一個(gè)引用來(lái)執(zhí)行。它為 console
對(duì)象進(jìn)行一個(gè) RHS 查詢(xún)挠锥,然后發(fā)生一個(gè)屬性解析來(lái)看它是否擁有一個(gè)稱(chēng)為 log
的方法众羡。
最后,我們可以將這一過(guò)程概念化為蓖租,在將值 2
(通過(guò)變量 a
的 RHS 查詢(xún)得到的)傳入 log(..)
時(shí)發(fā)生了一次 LHS/RHS 的交換粱侣。在 log(..)
的原生實(shí)現(xiàn)內(nèi)部,我們可以假定它擁有參數(shù)蓖宦,其中的第一個(gè)(也許被稱(chēng)為 arg1
)在 2
被賦值給它之前齐婴,進(jìn)行了一次 LHS 引用查詢(xún)。
注意: 你可能會(huì)試圖將函數(shù)聲明 function foo(a) {...
概念化為一個(gè)普通的變量聲明和賦值稠茂,比如 var foo
和 foo = function(a){...
尔店。這樣做會(huì)誘使你認(rèn)為函數(shù)聲明涉及了一次 LHS 查詢(xún)。
然而,一個(gè)微妙但重要的不同是嚣州,在這種情況下 編譯器 在代碼生成期間同時(shí)處理聲明和值的定義鲫售,如此當(dāng) 引擎 執(zhí)行代碼時(shí),沒(méi)有必要將一個(gè)函數(shù)值“賦予” foo
该肴。因此情竹,將函數(shù)聲明考慮為一個(gè)我們?cè)谶@里討論的 LHS 查詢(xún)賦值是不太合適的。
引擎/作用域?qū)υ?huà)
function foo(a) {
console.log( a ); // 2
}
foo( 2 );
讓我們將上面的(處理這個(gè)代碼段的)交互想象為一場(chǎng)對(duì)話(huà)匀哄。這場(chǎng)對(duì)話(huà)將會(huì)有點(diǎn)兒像這樣進(jìn)行:
引擎:嘿 作用域秦效,我有一個(gè)
foo
的 RHS 引用涎嚼。聽(tīng)說(shuō)過(guò)它嗎阱州?
作用域;啊法梯,是的苔货,聽(tīng)說(shuō)過(guò)。編譯器 剛在一秒鐘之前聲明了它立哑。它是一個(gè)函數(shù)夜惭。給你。
引擎:太棒了铛绰,謝謝诈茧!好的,我要執(zhí)行
foo
了捂掰。
引擎:嘿敢会,作用域,我得到了一個(gè)
a
的 LHS 引用这嚣,聽(tīng)說(shuō)過(guò)它嗎走触?
作用域:啊,是的疤苹,聽(tīng)說(shuō)過(guò)互广。編譯器 剛才將它聲明為
foo
的一個(gè)正式參數(shù)了。給你卧土。
引擎:一如既往的給力惫皱,作用域。再次感謝你∮容海現(xiàn)在旅敷,該把
2
賦值給a
了。
引擎:嘿颤霎,作用域媳谁,很抱歉又一次打擾你涂滴。我需要 RHS 查詢(xún)
console
。聽(tīng)說(shuō)過(guò)它嗎晴音?
作用域:沒(méi)關(guān)系柔纵,引擎,這是我一天到晚的工作锤躁。是的搁料,我得到
console
了。它是一個(gè)內(nèi)建對(duì)象系羞。給你郭计。
引擎:完美。查找
log(..)
椒振。好的昭伸,很好,它是一個(gè)函數(shù)澎迎。
引擎:嘿庐杨,作用域。你能幫我查一下
a
的 RHS 引用嗎嗡善?我想我記得它,但只是想再次確認(rèn)一下学歧。
作用域:你是對(duì)的罩引,引擎。同一個(gè)家伙枝笨,沒(méi)變袁铐。給你。
引擎:酷横浑。傳遞
a
的值剔桨,也就是2
,給log(..)
徙融。
...
小測(cè)驗(yàn)
檢查你到目前為止的理解洒缀。確保你扮演 引擎,并與 作用域 “對(duì)話(huà)”:
function foo(a) {
var b = a;
return a + b;
}
var c = foo( 2 );
找到所有的 LHS 查詢(xún)(有3處F奂健)树绩。
找到所有的 RHS 查詢(xún)(有4處!)隐轩。
注意: 小測(cè)驗(yàn)答案參見(jiàn)本章的復(fù)習(xí)部分饺饭!
嵌套的作用域
我們說(shuō)過(guò) 作用域 是通過(guò)標(biāo)識(shí)符名稱(chēng)查詢(xún)變量的一組規(guī)則。但是职车,通常會(huì)有多于一個(gè)的 作用域 需要考慮瘫俊。
就像一個(gè)代碼塊兒或函數(shù)被嵌套在另一個(gè)代碼塊兒或函數(shù)中一樣鹊杖,作用域被嵌套在其他的作用域中。所以扛芽,如果在直接作用域中找不到一個(gè)變量的話(huà)骂蓖,引擎 就會(huì)咨詢(xún)下一個(gè)外層作用域,如此繼續(xù)直到找到這個(gè)變量或者到達(dá)最外層作用域(也就是全局作用域)胸哥。
考慮這段代碼:
function foo(a) {
console.log( a + b );
}
var b = 2;
foo( 2 ); // 4
b
的 RHS 引用不能在函數(shù) foo
的內(nèi)部被解析涯竟,但是可以在它的外圍 作用域(這個(gè)例子中是全局作用域)中解析。
所以空厌,重返 引擎 和 作用域 的對(duì)話(huà)庐船,我們會(huì)聽(tīng)到:
引擎:“嘿,
foo
的 作用域嘲更,聽(tīng)說(shuō)過(guò)b
嗎筐钟?我得到一個(gè)它的 RHS 引用「畴”
作用域:“沒(méi)有篓冲,從沒(méi)聽(tīng)說(shuō)過(guò)。問(wèn)問(wèn)別人吧宠哄∫冀”
引擎:“嘿,
foo
外面的 作用域毛嫉,哦诽俯,你是全局 作用域,好吧承粤,酷暴区。聽(tīng)說(shuō)過(guò)b
嗎?我得到一個(gè)它的 RHS 引用辛臊∠闪唬”
作用域:“是的,當(dāng)然有彻舰。給你伐割。”
遍歷嵌套 作用域 的簡(jiǎn)單規(guī)則:引擎 從當(dāng)前執(zhí)行的 作用域 開(kāi)始刃唤,在那里查找變量口猜,如果沒(méi)有找到,就向上走一級(jí)繼續(xù)查找透揣,如此類(lèi)推济炎。如果到了最外層的全局作用域,那么查找就會(huì)停止辐真,無(wú)論它是否找到了變量须尚。
建筑的隱喻
為了將嵌套 作用域 解析的過(guò)程可視化崖堤,我想讓你考慮一下這個(gè)高層建筑。
<img src="fig1.png" width="250">
這個(gè)建筑物表示我們程序的嵌套 作用域 規(guī)則集合耐床。無(wú)論你在哪里密幔,建筑的第一層表示你當(dāng)前執(zhí)行的 作用域。建筑的頂層表示全局 作用域撩轰。
你通過(guò)在你當(dāng)前的樓層中查找來(lái)解析 LHS 和 RHS 引用胯甩,如果你沒(méi)有找到它,就坐電梯到上一層樓堪嫂,在那里尋找偎箫,然后再上一層,如此類(lèi)推皆串。一旦你到了頂層(全局 作用域)淹办,你要么找到了你想要的東西,要么沒(méi)有恶复。但是不管怎樣你都不得不停止了怜森。
錯(cuò)誤
為什么我們區(qū)別 LHS 和 RHS 那么重要?
因?yàn)樵谧兞窟€沒(méi)有被聲明(在所有被查詢(xún)的 作用域 中都沒(méi)找到)的情況下谤牡,這兩種類(lèi)型的查詢(xún)的行為不同副硅。
考慮如下代碼:
function foo(a) {
console.log( a + b );
b = a;
}
foo( 2 );
當(dāng) b
的 RHS 查詢(xún)第一次發(fā)生時(shí),它是找不到的翅萤。它被說(shuō)成是一個(gè)“未聲明”的變量恐疲,因?yàn)樗谧饔糜蛑姓也坏健?/p>
如果 RHS 查詢(xún)?cè)谇短椎?作用域 的任何地方都找不到一個(gè)值,這會(huì)導(dǎo)致 引擎 拋出一個(gè) ReferenceError
断序。必須要注意的是這個(gè)錯(cuò)誤的類(lèi)型是 ReferenceError
流纹。
相比之下糜烹,如果 引擎 在進(jìn)行一個(gè) LHS 查詢(xún)违诗,但到達(dá)了頂層(全局 作用域)都沒(méi)有找到它,而且如果程序沒(méi)有運(yùn)行在“Strict模式”[1]下疮蹦,那么這個(gè)全局 作用域 將會(huì)在 全局作用域中 創(chuàng)建一個(gè)同名的新變量诸迟,并把它交還給 引擎。
“不愕乎,之前沒(méi)有這樣的東西阵苇,但是我可以幫忙給你創(chuàng)建一個(gè)「新郏”
在 ES5 中被加入的“Strict模式”[1]绅项,有許多與一般/寬松/懶惰模式不同的行為。其中之一就是不允許自動(dòng)/隱含的全局變量創(chuàng)建比肄。在這種情況下快耿,將不會(huì)有全局 作用域 的變量交回給 LHS 查詢(xún)囊陡,并且類(lèi)似于 RHS 的情況, 引擎 將拋出一個(gè) ReferenceError
。
現(xiàn)在掀亥,如果一個(gè) RHS 查詢(xún)的變量被找到了撞反,但是你試著去做一些這個(gè)值不可能做到的事,比如將一個(gè)非函數(shù)的值作為函數(shù)運(yùn)行搪花,或者引用 null
或者 undefined
值的屬性遏片,那么 引擎 就會(huì)拋出一個(gè)不同種類(lèi)的錯(cuò)誤,稱(chēng)為 TypeError
撮竿。
ReferenceError
是關(guān)于 作用域 解析失敗的吮便,而 TypeError
暗示著 作用域 解析成功了,但是試圖對(duì)這個(gè)結(jié)果進(jìn)行了一個(gè)非法/不可能的動(dòng)作倚聚。
復(fù)習(xí)
作用域是一組規(guī)則线衫,它決定了一個(gè)變量(標(biāo)識(shí)符)在哪里和如何被查找。這種查詢(xún)也許是為了向這個(gè)變量賦值惑折,這時(shí)變量是一個(gè) LHS(左手邊)引用授账,或者是為取得它的值,這時(shí)變量是一個(gè) RHS(右手邊)引用惨驶。
LHS 引用得自賦值操作白热。作用域 相關(guān)的賦值可以通過(guò) =
操作符發(fā)生,也可以通過(guò)向函數(shù)參數(shù)傳遞(賦予)參數(shù)值發(fā)生粗卜。
JavaScript 引擎 在執(zhí)行代碼之前首先會(huì)編譯它屋确,因此,它將 var a = 2;
這樣的語(yǔ)句分割為兩個(gè)分離的步驟:
首先续扔,
var a
在當(dāng)前 作用域 中聲明攻臀。這是在最開(kāi)始,代碼執(zhí)行之前實(shí)施的纱昧。稍后刨啸,
a = 2
查找這個(gè)變量(LHS 引用),并且如果找到就向它賦值识脆。
LHS 和 RHS 引用查詢(xún)都從當(dāng)前執(zhí)行中的 作用域 開(kāi)始设联,如果有需要(也就是,它們?cè)谶@里沒(méi)能找到它們要找的東西)灼捂,它們會(huì)在嵌套的 作用域 中一路向上离例,一次一個(gè)作用域(層)地查找這個(gè)標(biāo)識(shí)符,直到它們到達(dá)全局作用域(頂層)并停止悉稠,既可能找到也可能沒(méi)找到宫蛆。
未被滿(mǎn)足的 RHS 引用會(huì)導(dǎo)致 ReferenceError
被拋出。未被滿(mǎn)足的 LHS 引用會(huì)導(dǎo)致一個(gè)自動(dòng)的的猛,隱含地創(chuàng)建的同名全局變量(如果不是“Strict模式”[1])耀盗,或者一個(gè) ReferenceError
(如果是“Strict模式”[1])辑甜。
小測(cè)驗(yàn)答案
function foo(a) {
var b = a;
return a + b;
}
var c = foo( 2 );
-
找出所有的 LHS 查詢(xún)(有3處!)袍冷。
c = ..
,a = 2
(隱含的參數(shù)賦值)和b = ..
-
找出所有的 RHS 查詢(xún)(有4處A状住)。
foo(2..
,= a;
,a + ..
和.. + b
-
MDN: Strict Mode ? ? ? ?