作用域是什么
參考和摘錄自《你不知道的JavaScript(上)》
編譯原理
通常將 JavaScript 歸類為“動態(tài)”或“解釋執(zhí)行”語言寂纪,但事實上它是一門編譯語言栈拖。但與傳統(tǒng)的編譯語言不同绽族,它不是提前編譯的者疤,編譯結(jié)果也不能在分布式系統(tǒng)中進行移植苫纤。
【傳統(tǒng)編譯語言編譯步驟】:
- 分詞/詞法分析(Tokenizing/Lexing):這個過程會將由字符組成的字符串分解成有意義(對編程語言來說)的代碼塊互订,這些代碼塊被稱為詞法單元(token)吱肌。例如,var a = 2;仰禽。這段程序通常會被分解為如下詞法單元:var氮墨、a、=吐葵、2规揪、;∥虑停空格是否會被當(dāng)作詞法單元猛铅,取決于空格在這門語言中是否具有意義。
- 解析/語法分析(Parsing):將詞法單元流(數(shù)組)轉(zhuǎn)換成一個由元素逐級嵌套所組成的代表了程序語法結(jié)構(gòu)的樹凤藏。這個樹被稱為“抽象語法樹”(Abstract Syntax Tree奸忽,AST)。
- 代碼生成:將 AST 轉(zhuǎn)換為可執(zhí)行代碼的過程被稱為代碼生成揖庄。這個過程與語言栗菜、目標(biāo)平臺等息息相關(guān)。拋開具體細(xì)節(jié)蹄梢,簡單來說就是有某種方法可以把 var a = 2; 的 AST 轉(zhuǎn)化為一組機器指令疙筹,用來創(chuàng)建一個叫作 a 的變量(包括分配內(nèi)存等),并將一個值儲存在 a 中。
比起那些編譯過程只有三個步驟的語言的編譯器腌歉,JavaScript 引擎要復(fù)雜得多蛙酪。例如,在語法分析和代碼生成階段有特定的步驟來對運行性能進行優(yōu)化翘盖,包括對冗余元素進行優(yōu)化等桂塞。
首先,JavaScript 引擎不會有大量的(像其他語言編譯器那么多的)時間用來進行優(yōu)化馍驯,因為與其他語言不同阁危,JavaScript 的編譯過程不是發(fā)生在構(gòu)建之前的。對于 JavaScript 來說汰瘫,大部分情況下編譯發(fā)生在代碼執(zhí)行前的幾微妙(甚至更短?翊颉)的時間內(nèi)。
簡單地說混弥,任何 JavaScript 代碼片段在執(zhí)行前都要進行編譯(通常就在執(zhí)行前)趴乡。因此,JavaScript 編譯器首先會對 var a = 2; 這段程序進行編譯蝗拿,然后做好執(zhí)行它的準(zhǔn)備晾捏,并且通常馬上就會執(zhí)行它。
理解作用域
學(xué)習(xí)作用域的方式是將這個過程模擬成幾個人物之間的對話哀托。
演員表
- 引擎:從頭到尾負(fù)責(zé)整個 JavaScript 程序的編譯及執(zhí)行過程惦辛。
- 編譯器:引擎的好朋友之一,負(fù)責(zé)語法分析及代碼生成等工作仓手。
- 作用域:引擎的另一位好朋友胖齐,負(fù)責(zé)收集并維護由所有聲明的標(biāo)識符(變量)組成的一系列查詢,并實施一套非常嚴(yán)格的規(guī)則嗽冒,確定當(dāng)前執(zhí)行的代碼對這些標(biāo)識符的訪問權(quán)限呀伙。
對話(舉例)
在執(zhí)行 var a = 2; 這段代碼時,引擎會認(rèn)為這里有兩個完全不同的聲明辛慰,一個由編譯器在編譯時處理区匠,另一個則由引擎在運行時 處理。
編譯器首先會將這段程序分解成詞法單元帅腌,然后將詞法單元解析成一個樹結(jié)構(gòu)驰弄,最后開始進行代碼生成:
- 遇到 var a,編譯器會詢問作用域是否已經(jīng)有一個該名稱的變量存在于同一個作用域的集合中速客。如果是戚篙,編譯器會忽略該聲明,繼續(xù)進行編譯溺职;否則它會要求作用域在當(dāng)前作用域的集合中聲明一個新的變量岔擂,并命名為 a位喂。
- 接下來編譯器會為引擎生成運行時所需的代碼,這些代碼被用來處理 a = 2 這個賦值操作。引擎運行時會首先詢問作用域,在當(dāng)前的作用域集合中是否存在一個叫作 a 的變量侵俗。如果是,引擎就會使用這個變量规婆;如果否,引擎會繼續(xù)查找該變量蝉稳。如果最終找到了這個變量抒蚜,就會將 a 賦值給它。否則引擎就會拋出一個異常耘戚。
【總結(jié)】:變量的賦值操作會執(zhí)行兩個動作嗡髓,首先編譯器會在當(dāng)前作用域中聲明一個變量(如果之前沒有聲明過),然后在運行時引擎會在作用域中查找該變量收津,如果能夠找到就會對它賦值饿这。
編譯器有話說
引擎在查找變量的過程由左右能夠與進行協(xié)助,但是引擎執(zhí)行怎樣的查找撞秋,會影響最終的查找結(jié)果蛹稍。
【查找類型】:
- LHS 查詢:賦值操作的目標(biāo)是誰?
- RHS 查詢:誰是賦值操作的源頭部服?
【L 和 R 的含義】:分別代表變量出現(xiàn)的位置在賦值操作的左側(cè)和右側(cè)。也就是說拗慨,當(dāng)變量出現(xiàn)在賦值操作的左側(cè)時廓八,執(zhí)行 LHS 查詢。當(dāng)變量出現(xiàn)在賦值操作的右側(cè)時赵抢,執(zhí)行 RHS 查詢剧蹂。
【注意】:RHS 查詢與簡單地查找某個變量的值別無二致,而 LHS 查詢則是試圖找到變量的容器本身烦却,從而可以對其賦值宠叼。從這個角度說,RHS 并不是真正意義上的“賦值操作的右側(cè)”其爵,更準(zhǔn)確地說“非左側(cè)”冒冬。可以將其理解成 retrieve his source value(取到它的源值)摩渺,這意味著“得到某某的值”简烤。
【示例】:
console.log(a); // RHS 引用
a = 2; // LHS 引用
function foo(a) {
console.log(a); // 2
}
foo(2);
【解釋】:
- 執(zhí)行 foo() 函數(shù)的調(diào)用需要對 foo 進行 RHS 引用。
- 參數(shù)傳遞過程中的隱式分配摇幻,此時進行 LHS 查詢横侦。
- console.log() 本身也需要一個引用才能執(zhí)行挥萌,因此會對 console 對象進行 RHS 查詢,并且檢查得到的值中是否有一個叫作 log 的方法(RHS 查詢)
- console.log(a) 對 a 進行 RHS 引用枉侧。
【對話的形式來解釋】:
引擎:我說作用域引瀑,我需要為 foo 進行 RHS 引用。你見過它嗎榨馁?
作用域:別說憨栽,我還真見過,編譯器那小子剛剛聲明了它辆影。它是一個函數(shù)徒像,給你。
引擎:哥們太夠意思了蛙讥!好吧锯蛀,我來執(zhí)行一下 foo。
引擎:作用域次慢,還有個事兒旁涤。我需要為 a 進行 LHS 引用,這個你見過嗎迫像?
作用域:這個也見過劈愚,編譯器最近把它聲明為 foo 的一個形式參數(shù)了,拿去吧闻妓。
引擎:大恩不言謝菌羽,你總是這么棒。現(xiàn)在我要把 2 賦值給 a由缆。
引擎:哥們注祖,不好意思又來打擾你。我要為 console 進行 RHS 引用均唉,你見過它嗎是晨?
作用域:咱倆誰跟誰啊,再說我就是干這個舔箭。這個我也有罩缴,console 是個內(nèi)置對象。給你层扶。
引擎:么么噠箫章。我得看看這里面是不是有 log()。太好了怒医,找到了炉抒,是一個函數(shù)。
引擎:哥們稚叹,能幫我再找一下對 a 的 RHS 引用嗎焰薄?雖然我記得它拿诸,但想再確認(rèn)一次。
作用域:放心吧塞茅,這個變量沒有變動過亩码,拿走,不懈野瘦。
引擎:真棒描沟。我來把 a 的值,也就是 2鞭光,傳遞進 log()吏廉。
......
作用域嵌套
作用域是根據(jù)名稱查找變量的一套規(guī)則。實際情況下惰许,通常需要同時顧及幾個作用域席覆。
當(dāng)一個塊或函數(shù)嵌套在另一個塊或函數(shù)中時,就發(fā)生了作用域的嵌套汹买。因此佩伤,在當(dāng)前作用域中無法找到某個變量時,引擎就會在外層嵌套的作用域中繼續(xù)查找晦毙,直到找到該變量生巡,或抵達(dá)最外層的作用域(也就是全局作用域)為止。
function foo(a) {
console.log(a + b);
}
var b = 2;
foo(2); // 4
對 b 進行的 RHS 引用無法在函數(shù) foo 內(nèi)部完成见妒,但可以在上一級作用域(在上面例子中是在全局作用域中)中完成孤荣。
【小劇場】:
引擎:foo 的作用域兄弟,你見過 b 嗎须揣?我需要對它進行 RHS 引用垃环。
作用域:聽都沒聽過,走開返敬。
引擎:foo 的上級作用域兄弟,咦寥院?有眼不識泰山劲赠,原來你是全局作用域大哥,太好了秸谢。你見過 b 嗎凛澎?我需要對它進行 RHS 引用。
作用域:當(dāng)然了估蹄,給你吧塑煎。
遍歷嵌套作用域規(guī)則:引擎從當(dāng)前的執(zhí)行作用域開始查找變量。如果找不到臭蚁,就向上一級繼續(xù)查找最铁。當(dāng)?shù)诌_(dá)最外層的全局作用域時讯赏,無論找到還是沒找到,查找過程都會停止冷尉。
異常
【問】:為什么區(qū)分 LHS 和 RHS 是一件重要的事情漱挎?
【答】:因為在變量還沒有聲明(在任何作用域中都無法找到該變量)的情況下,這兩種查詢的行為是不一樣的雀哨。
舉例說明
function foo(a) {
console.log(a + b);
b = a;
}
foo(2);
第一次對 b 進行 RHS 查詢時是無法找到該變量的磕谅。也就是說,這是一個“未聲明”的變量雾棺,因為在任何相關(guān)的作用域中都無法找到它膊夹。
如果 RHS 查詢在所有嵌套的作用域中遍尋不到所需的變量,引擎就會拋出 ReferenceError 異常捌浩。
【注意】:ReferenceError 是非常重要的異常類型放刨。
相較之下,當(dāng)引擎執(zhí)行 LHS 查詢時嘉栓,如果在頂層(全局作用域)中也無法找到目標(biāo)變量宏榕,全局作用域中就會創(chuàng)建一個具有該名稱的變量,并將其返還給引擎侵佃,前提是程序運行在非“嚴(yán)格模式”下麻昼。
作用域:“不,這個變量之前并不存在馋辈,但是我很熱心地幫你創(chuàng)建了一個抚芦。”
ES5 中引入了“嚴(yán)格模式”迈螟。同正常模式叉抡,或者說寬松、懶惰模式答毫,嚴(yán)格模式在行為上有很多不同褥民。其中一個不同的行為是嚴(yán)格模式禁止自動或隱式地創(chuàng)建全局變量。因此洗搂,在嚴(yán)格模式中 LHS 查詢失敗時消返,并不會創(chuàng)建并返回一個全局變量,引擎會拋出同 RHS 查詢失敗時類似的 ReferenceError 異常耘拇。
接下來撵颊,如果 RHS 查詢找到了一個變量,但是你嘗試對這個變量的值進行不合理的操作惫叛,比如試圖對一個非函數(shù)類型的值進行函數(shù)調(diào)用倡勇,或者引用 null 或 undefined 類型的值中的屬性,那么引擎會拋出另外一種類型的異常嘉涌,叫作 TypeError妻熊。
ReferenceError 同作用域判別失敗相關(guān)夸浅,而 TypeError 則代表作用域判別成功了,但是對結(jié)果的操作是非法或不合理的固耘。
小結(jié)
- 作用域是一套規(guī)則题篷,用于確定在何處以及如何查找變量(標(biāo)識符)。如果查找的目的是對變量進行賦值厅目,那么就會使用 LHS 查詢番枚;如果目的是獲取變量的值,就會使用 RHS 查詢损敷。賦值操作符會導(dǎo)致 LHS 查詢葫笼。= 操作符或調(diào)用函數(shù)時傳入?yún)?shù)的操作都會導(dǎo)致關(guān)聯(lián)作用域的賦值操作。
- JavaScript 引擎首先會在代碼執(zhí)行前對其進行編譯拗馒,在這個過程中路星,像 var a = 2; 這樣的聲明會被分解成兩個獨立的步驟:
- 首先,var a 在其作用域中聲明新變量诱桂。這會在最開始的階段洋丐,也就是代碼執(zhí)行前進行。
- 接下來挥等,a = 2 會查詢(LHS 查詢)變量 a 并對其進行賦值友绝。
- LHS 和 RHS 查詢都會在當(dāng)前執(zhí)行作用域中開始,如果有需要(也就是說它們沒有找到所需的標(biāo)識符)肝劲,就會向上級作用域繼續(xù)查找目標(biāo)標(biāo)識符迁客,這樣每次上升一級作用域,最后抵達(dá)全局作用域辞槐,無論找到或沒找到都將停止掷漱。
- 不成功的 RHS 引用會導(dǎo)致拋出 ReferenceError 異常。不成功的 LHS 引用會導(dǎo)致自動隱式地創(chuàng)建一個全局變量(非嚴(yán)格模式下)榄檬,該變量使用 LHS 引用的目標(biāo)作為標(biāo)識符卜范,或者拋出 ReferenceError 異常(嚴(yán)格模式下)。