第1章:作用域是什么
- 我們通過(guò)
var
聲明變量時(shí)壕吹,是否考慮過(guò)這些問題:- 這些變量都存儲(chǔ)在哪里?
- 程序用到它們時(shí),又是怎么找到它們的刽沾?
- 而答案就是:不僅僅是JavaScript,任何編程語(yǔ)言都會(huì)設(shè)計(jì)一套良好的規(guī)則來(lái)存取變量排拷,而這套規(guī)則就叫做 作用域侧漓。
1.1 編譯原理
- 雖然和靜態(tài)語(yǔ)言(比如Java)不同,JavaScript是“解釋性”的動(dòng)態(tài)語(yǔ)言监氢。
- 但實(shí)際上布蔗,JavaScript代碼在運(yùn)行之前也是需要編譯的,并且JavaScript引擎編譯的步驟浪腐,和傳統(tǒng)的編譯語(yǔ)言非常相似纵揍,大致有以下三大步驟:
第1步:分詞/詞法分析(Tokenizing/Lexing)
- 任何
.js
文件在解析前,對(duì)于JS引擎而言都是一大段文本议街,不能直接運(yùn)行泽谨。所以當(dāng)務(wù)之急,就是將文本字符串“大卸八塊”般的進(jìn)行分解傍睹。 - 詞法分析就是 將文本內(nèi)容分解成有意義的詞法字符串(token) 隔盛。
- 比如
var a = 2;
最終會(huì)分解成詞法字符串?dāng)?shù)組,得到 [var
拾稳、a
吮炕、=
、2
访得、;
]龙亲,而多余的空格則是無(wú)意義的陕凹。
第2步:解析/語(yǔ)法分析(Parsing)
- 語(yǔ)法分析則是 將詞法字符串?dāng)?shù)組轉(zhuǎn)換成 “抽象語(yǔ)法樹”(Abstract Syntax Tree,AST)
- 比如代碼
var a = 2;
會(huì)生成以下具有層次結(jié)構(gòu)的對(duì)象/*變量聲明的對(duì)象*/ VariableDeclaration : { /*變量名為 a*/ Identifier : a, /*變量賦值表達(dá)式*/ AssignmentExpression : { /*數(shù)值類型為 2*/ NumericLiteral : 2 } }
第3步:代碼生成
- 最后一步就是生成代碼鳄炉, 將AST轉(zhuǎn)換為可執(zhí)行的機(jī)器指令 杜耙。
- 比如代碼
var a = 2;
會(huì)創(chuàng)建一個(gè)變量a
,并為其分配內(nèi)存拂盯,然后將值2
存進(jìn)這個(gè)變量佑女。
1.2 理解作用域
原書將引擎、編譯器以及作用域模擬成三個(gè)演員谈竿,用來(lái)說(shuō)明在執(zhí)行一段代碼時(shí)团驱,三者分別負(fù)責(zé)的工作。但我稍微做一些改動(dòng)空凸,將作用域比喻成一個(gè)記錄清單嚎花。
- 執(zhí)行JS代碼依賴三個(gè)東西:
-
引擎
:負(fù)責(zé)JS代碼的編譯和執(zhí)行 -
編譯器
:在引擎工作前,負(fù)責(zé)語(yǔ)法分析和代碼生成 -
作用域
:一個(gè)具有嚴(yán)格的規(guī)則呀洲,專門負(fù)責(zé)收集并維護(hù)所有變量的清單列表紊选,通過(guò)它來(lái)存取變量
-
- 閱讀代碼
var a = 2;
其實(shí)訪問了兩次作用域,一個(gè)是 在編譯器編譯時(shí)檢查變量聲明道逗,一個(gè)是 引擎運(yùn)行時(shí)檢查使用:- 如上面所說(shuō)的兵罢,第1步編譯器會(huì)進(jìn)行詞法分析,第2步將詞法單元解析成一個(gè)樹結(jié)構(gòu)的對(duì)象憔辫;
- 在第3步生成代碼時(shí)趣些,編譯器會(huì)去查找作用域,檢查 是否存在同名的變量贰您,如果沒有則聲明一個(gè)新的變量并賦值 坏平;
- 最后引擎運(yùn)行代碼時(shí),會(huì)再次通過(guò)作用域 檢查 是否存在同名的變量锦亦,如果有則直接 使用舶替,沒有則繼續(xù)向上查找
- 引擎執(zhí)行代碼到作用域查找變量,分為兩種類型:RHS查詢 和 LHS查詢:
- “L(left)”和“R(right)”分別代表變量處于表達(dá)式的左邊還是右邊杠园;
- RHS查詢就是查找變量顾瞪,可理解成retrieve his source value(找到它源值)。比如
console.log(a)
就是RHS查詢抛蚁,找到變量a
的值傳遞給console.log()
陈醒; - LHS查詢則是查找變量的容器對(duì)其進(jìn)行賦值。比如
var a = 2;
就是LHS查詢瞧甩,找到變量a
并為它賦值= 2
钉跷;
- 我們嘗試用RHS查詢和LHS查詢的思維來(lái)閱讀JS代碼:
我們都知道function foo(a){ console.log(a); } foo(2);
function
聲明函數(shù)的方式等同于,聲明一個(gè)變量并為其賦值一個(gè)執(zhí)行方法體:var foo = function(a){ console.log(a); } foo(2);
-
var foo = function()
這是一個(gè)LHS查詢:聲明foo
變量并為其賦值一個(gè)方法肚逸; -
foo(2)
屬于RHS查詢:找到foo
變量的值并執(zhí)行它 - 進(jìn)到
foo
方法體中爷辙,實(shí)際上這里隱藏了一句代碼a = 2;
將傳遞的值賦值給形參 -
console.log(a)
是RHS查詢:找到a
的值彬坏,傳遞給console.log(...)
- 值得一提的是,
console.log()
本身也屬于RHS查詢膝晾,會(huì)去找尋log()
方法的引用并執(zhí)行它
-
1.3 作用域嵌套
- 不管是RHS查詢還是LHS查詢都從當(dāng)前作用域開始栓始,如果當(dāng)前作用域無(wú)法找到變量時(shí),引擎會(huì)轉(zhuǎn)移到外層作用域中繼續(xù)查找血当,直至轉(zhuǎn)移到最頂層的作用域幻赚,也就是全局作用域。
- 舉例:
在function foo(a){ console.log(a + b); } var b = 2; foo(2);
foo
方法體中歹颓,變量b
在foo
的作用域中找不到坯屿,將會(huì)到外層的全局作用域查找,最后輸出4
1.4 異常
- 之所以 區(qū)分RHS和LHS巍扛,是因?yàn)楫?dāng)查找到未聲明的變量時(shí),這兩種查詢的行為是不一樣的:
- 如前文提到的乏德,LHS查詢失敗時(shí)會(huì)在全局作用域創(chuàng)建一個(gè)同名的變量撤奸;
- 而RHS查詢失敗時(shí),則會(huì)拋出 ReferenceError異常喊括;另一種情況是胧瓜,查找到了變量,但是嘗試對(duì)這個(gè)變量的值做不合理的操作(比如對(duì)一個(gè)非函數(shù)的變量進(jìn)行調(diào)用)郑什,則拋出TypeError異常
- 總而言之府喳,RererenceError異常是作用域判別失敗相關(guān)的, TypeError異常 則代表作用域判別成功了蘑拯,但對(duì)結(jié)果的操作是非法或不合理的
1.5 小結(jié)
- 作用域是一套存取變量的規(guī)則钝满;
- 在代碼執(zhí)行前,會(huì)先由編譯器進(jìn)行編譯申窘,JavaScript引擎在執(zhí)行代碼時(shí)會(huì)進(jìn)行LHS查詢和RHS查詢:
-
LHS查詢是對(duì)變量進(jìn)行賦值弯蚜,其中
=
操作符或者調(diào)用函數(shù)時(shí)傳參的操作,都會(huì)導(dǎo)致相關(guān)作用域的賦值操作剃法; - RHS查詢是對(duì)變量的值進(jìn)行查找碎捺;
-
LHS查詢是對(duì)變量進(jìn)行賦值弯蚜,其中
- LHS和RHS查詢都會(huì)從當(dāng)前執(zhí)行作用域開始,如果當(dāng)前作用域找不到贷洲,就會(huì)往上級(jí)作用域繼續(xù)查找收厨,每次上升一級(jí)作用域,直至到頂級(jí)的全局作用域
- 不成功的RHS查詢會(huì)拋出Reference異常优构,而不成功的LHS查詢會(huì)自動(dòng)式地創(chuàng)建一個(gè)全局變量