什么是JavaScript的作用域
作用域的概念
現(xiàn)代編程語言的最基本功能之一就是能夠存儲(chǔ)變量當(dāng)中的值,以便于之后的使用于修改。也正是這個(gè)功能將狀態(tài)帶給了程序。
在JavaScript中,作用域就是一套設(shè)計(jì)良好的規(guī)則來存儲(chǔ)變量。
簡(jiǎn)述編譯原理
通常我們會(huì)將JavaScript歸類為“動(dòng)態(tài)”或“解釋執(zhí)行“語言,但它實(shí)際上是一門編譯語言。與傳統(tǒng)的編譯語言不同,它不是提前編譯的,編譯結(jié)果也不能在分布式系統(tǒng)中進(jìn)行移植。
例如V8引擎,為了提高JavaScript代碼的運(yùn)行性能,在運(yùn)行之前會(huì)先將其編譯為本地的機(jī)器碼,然后再去執(zhí)行機(jī)器碼,達(dá)到提升速度的目的。
- 分詞/詞法分析
這個(gè)過程將由字符組成的代碼分解成對(duì)程序有意義的代碼塊,這些代碼塊被稱為詞法單元。
例如 var foo = 'bar' 通常會(huì)被分解為這些詞法單元:var 、 foo 、 = 、 'bar'
- 解析/語法分析
這個(gè)過程將詞法單元轉(zhuǎn)換成一個(gè)“由元素逐級(jí)嵌套組成的代表程序語法的樹“,這個(gè)樹被稱為“抽象語法樹”(AST)。
image
- 代碼生成
將上邊的抽象語法樹轉(zhuǎn)換為機(jī)器可執(zhí)行代碼
JavaScript引擎比只有三個(gè)步驟的語言的編譯器要復(fù)雜的多。例如在語法分析和代碼生成階段有特定的步驟來對(duì)運(yùn)行性能進(jìn)行優(yōu)化,包括對(duì)冗余元素進(jìn)行優(yōu)化等。
對(duì)于JavaScript來說,大部分情況下編譯發(fā)生在代碼執(zhí)行的前幾微秒,任何代碼片段在執(zhí)行前都要進(jìn)行編譯。因此JavaScript編譯器首先對(duì) var foo = 'bar' 進(jìn)行編譯,然后做好執(zhí)行它的準(zhǔn)備,并且通常馬上就會(huì)執(zhí)行它。
引擎、編譯器、作用域在賦值操作中的配合
- 引擎:從頭到尾負(fù)責(zé)整個(gè)JavaScript程序編譯及執(zhí)行過程
- 編譯器:負(fù)責(zé)語法分析及代碼生成
- 作用域:負(fù)責(zé)收集維護(hù)由所有變量組成的一系列查詢
對(duì)于 var foo = 'bar' 這段代碼,大家很有可能認(rèn)為是一句簡(jiǎn)單的聲明。而事實(shí)上JavaScript執(zhí)行時(shí)會(huì)將它分成兩個(gè)完全不同的聲明。
- 編譯器首先將這段代碼分解成詞法單元,然后解析為樹結(jié)構(gòu)。(在下一步代碼生成時(shí),處理這段代碼的方式會(huì)跟預(yù)期有所不同)
- 遇到 var foo ,編譯器會(huì)檢查作用域是否已有同名變量存在。如果有的話編譯器會(huì)忽略聲明,繼續(xù)編譯。否則它會(huì)生成代碼在當(dāng)前作用域的變量集合中聲明一個(gè)新的變量,命名為 foo
- 接下來編譯器會(huì)為引擎生成運(yùn)行時(shí)所需代碼,用來處理 foo = 'bar' 這個(gè)賦值操作。
- 引擎運(yùn)行時(shí)會(huì)首先查詢當(dāng)前作用域是否存在叫做 foo 的變量。如果有引擎則會(huì)使用這個(gè)變量,否則會(huì)一直向上層作用域查找。
- 如果最終找到了 foo 這個(gè)變量,就會(huì)將 'bar' 賦給它,否則拋出異常。
總結(jié):變量的賦值會(huì)執(zhí)行兩個(gè)動(dòng)作:首先是編譯器在當(dāng)前作用域中聲明變量(如果變量未被聲明過);接著運(yùn)行時(shí)引擎在作用域查找該變量,能找到就會(huì)對(duì)它賦值。
LHS查詢 vs RHS查詢
引擎執(zhí)行編譯器生成的代碼時(shí),會(huì)通過查找 foo 來判斷是否已經(jīng)聲明過。查找的過程由作用域來協(xié)助。在我們的例子中,引擎為變量 foo 進(jìn)行的時(shí)LHS查詢,還有另一個(gè)查找類型叫RHS查詢。顧名思義,它們的意思是Left hand side 和 Right hand side
- LHS:變量出現(xiàn)在賦值操作的左側(cè)(查找賦值操作的目標(biāo)是誰)
- RHS:變量在其他位置出現(xiàn)(查找值的源頭)
- // 考慮下邊的代碼
- console.log(foo)
此例中 foo 的引用就是RHS查詢,這里沒有賦予 foo 任何值,相反的,我們需要查找 foo 的值,才能傳遞給log方法。
- // 相比之下
- foo = 'bar'
這里對(duì) foo 的查詢則是LHS查詢,我們并不關(guān)心 foo 當(dāng)前的值是什么, 只是想為這個(gè)賦值操作找到目標(biāo)。
- // 再分析下邊的代碼
- function foo(a) {
- console.log(a)
- }
- foo('bar')
這段代碼里既有LHS查詢又有RHS查詢
- 最后一行 foo(...) 函數(shù)的調(diào)用需要對(duì) foo 進(jìn)行RHS查詢 → 找到 foo 的值
- 入?yún)r(shí)存在隱式的 a = 'bar' ,需要對(duì) a 進(jìn)行LHS查詢
- console.log(a) 對(duì) a 進(jìn)行RHS查詢
- console.log(...) 本身也需要對(duì) console 對(duì)象進(jìn)行RHS查詢
作用域的嵌套
我們?cè)谖恼麻_始時(shí)說過,作用域是根據(jù)名稱查找變量的一套規(guī)則。實(shí)際情況中需要同時(shí)顧及幾個(gè)作用域。
當(dāng)一個(gè)塊或函數(shù)嵌套在另一個(gè)塊或函數(shù)中時(shí),就發(fā)生了作用域的嵌套。因此在當(dāng)前作用域中沒有查找到目標(biāo)變量時(shí),會(huì)逐層向上查找直到全局作用域。
- // 考慮以下代碼
- function foo(a) {
- console.log(a + b)
- }
- var b = 258;
- foo(369)
對(duì) b 進(jìn)行的RHS查詢無法在 foo 內(nèi)部完成,但可以在上一級(jí)的作用域中完成(在此例中是全局作用域)。
LHS,RHS查詢都會(huì)在作用域內(nèi)逐層查找,直到找到為止(或到達(dá)全局作用域)。
ReferenceError
上一節(jié)提到了LHS,RHS都會(huì)在作用域內(nèi)逐層查找變量,但如果到達(dá)全局作用域仍然沒有找到變量怎么辦呢?
這時(shí)區(qū)分LHS和RHS查詢的意義就體現(xiàn)出來了。
如果RHS查詢?cè)谒星短椎淖饔糜蛑卸紱]有找到所需變量,引擎就會(huì)拋出 ReferenceError。
如果LHS查詢?cè)谒星短椎淖饔糜蛑卸紱]有找到所需變量,引擎就會(huì)在全局作用域中創(chuàng)建一個(gè)具有該名稱的變量,并將其返回給引擎。
注意:ES5中引入了嚴(yán)格模式,與普通模式相比,嚴(yán)格模式其中一個(gè)不同就是進(jìn)制自動(dòng)或隱式的創(chuàng)建全局變量。因此在嚴(yán)格模式下LHS查詢失敗時(shí)不會(huì)創(chuàng)建并返回全局變量,引擎同樣會(huì)拋出 ReferenceError。
總結(jié)
- 作用域是一套規(guī)則,用于確定在何處以及如何查找變量。如果查找的目的是對(duì)變量賦值,會(huì)使用LHS查詢;如果目的是獲取變量的值,會(huì)使用RHS查詢。
- JavaScript引擎會(huì)在代碼執(zhí)行前對(duì)其進(jìn)行編譯。在這個(gè)過程中,像 var foo = 'bar' 這種聲明會(huì)被分解成兩個(gè)獨(dú)立的步驟。
1. var foo 在其作用域中聲明新的變量。此操作在代碼執(zhí)行前進(jìn)行。
2. 接下來 foo = 'bar' 會(huì)查詢(LHS)變量 foo 并對(duì)其賦值。
- LHS和RHS查詢都會(huì)在當(dāng)前執(zhí)行作用域中開始,如果有需要(沒有在當(dāng)前作用域找到變量)就會(huì)向上級(jí)作用域繼續(xù)查找目標(biāo)變量,一直抵達(dá)全局作用域,無論找到與否都會(huì)停止。
- 不成功的RHS查找會(huì)導(dǎo)致拋出 ReferenceError ,不成功的LHS查找會(huì)導(dǎo)致自動(dòng)隱式地創(chuàng)建一個(gè)全局變量(非嚴(yán)格模式下),或者拋出 ReferenceError(嚴(yán)格模式下)。

























