第1章:作用域是什么
- 我們通過(guò)
var聲明變量時(shí),是否考慮過(guò)這些問(wèn)題:- 這些變量都存儲(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ǔ)法樹(shù)”(Abstract Syntax Tree,AST)
- 比如代碼
var a = 2;會(huì)生成以下具有層次結(jié)構(gòu)的對(duì)象/*變量聲明的對(duì)象*/ VariableDeclaration : { /*變量名為 a*/ Identifier : a, /*變量賦值表達(dá)式*/ AssignmentExpression : { /*數(shù)值類(lèi)型為 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 理解作用域
原書(shū)將引擎、編譯器以及作用域模擬成三個(gè)演員,用來(lái)說(shuō)明在執(zhí)行一段代碼時(shí),三者分別負(fù)責(zé)的工作。但我稍微做一些改動(dòng),將作用域比喻成一個(gè)記錄清單。
- 執(zhí)行JS代碼依賴(lài)三個(gè)東西:
-
引擎:負(fù)責(zé)JS代碼的編譯和執(zhí)行 -
編譯器:在引擎工作前,負(fù)責(zé)語(yǔ)法分析和代碼生成 -
作用域:一個(gè)具有嚴(yán)格的規(guī)則,專(zhuān)門(mén)負(fù)責(zé)收集并維護(hù)所有變量的清單列表,通過(guò)它來(lái)存取變量
-
- 閱讀代碼
var a = 2;其實(shí)訪問(wèn)了兩次作用域,一個(gè)是 在編譯器編譯時(shí)檢查變量聲明,一個(gè)是 引擎運(yùn)行時(shí)檢查使用:- 如上面所說(shuō)的,第1步編譯器會(huì)進(jìn)行詞法分析,第2步將詞法單元解析成一個(gè)樹(shù)結(jié)構(gòu)的對(duì)象;
- 在第3步生成代碼時(shí),編譯器會(huì)去查找作用域,檢查 是否存在同名的變量,如果沒(méi)有則聲明一個(gè)新的變量并賦值 ;
- 最后引擎運(yùn)行代碼時(shí),會(huì)再次通過(guò)作用域 檢查 是否存在同名的變量,如果有則直接 使用,沒(méi)有則繼續(xù)向上查找
- 引擎執(zhí)行代碼到作用域查找變量,分為兩種類(lèi)型:RHS查詢(xún) 和 LHS查詢(xún):
- “L(left)”和“R(right)”分別代表變量處于表達(dá)式的左邊還是右邊;
- RHS查詢(xún)就是查找變量,可理解成retrieve his source value(找到它源值)。比如
console.log(a)就是RHS查詢(xún),找到變量a的值傳遞給console.log(); - LHS查詢(xún)則是查找變量的容器對(duì)其進(jìn)行賦值。比如
var a = 2;就是LHS查詢(xún),找到變量a并為它賦值= 2;
- 我們嘗試用RHS查詢(xún)和LHS查詢(xún)的思維來(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查詢(xún):聲明foo變量并為其賦值一個(gè)方法; -
foo(2)屬于RHS查詢(xún):找到foo變量的值并執(zhí)行它 - 進(jìn)到
foo方法體中,實(shí)際上這里隱藏了一句代碼a = 2;將傳遞的值賦值給形參 -
console.log(a)是RHS查詢(xún):找到a的值,傳遞給console.log(...) - 值得一提的是,
console.log()本身也屬于RHS查詢(xún),會(huì)去找尋log()方法的引用并執(zhí)行它
-
1.3 作用域嵌套
- 不管是RHS查詢(xún)還是LHS查詢(xún)都從當(dāng)前作用域開(kāi)始,如果當(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í),這兩種查詢(xún)的行為是不一樣的:
- 如前文提到的,LHS查詢(xún)失敗時(shí)會(huì)在全局作用域創(chuàng)建一個(gè)同名的變量;
- 而RHS查詢(xún)失敗時(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查詢(xún)和RHS查詢(xún):
-
LHS查詢(xún)是對(duì)變量進(jìn)行賦值,其中
=操作符或者調(diào)用函數(shù)時(shí)傳參的操作,都會(huì)導(dǎo)致相關(guān)作用域的賦值操作; - RHS查詢(xún)是對(duì)變量的值進(jìn)行查找;
-
LHS查詢(xún)是對(duì)變量進(jìn)行賦值,其中
- LHS和RHS查詢(xún)都會(huì)從當(dāng)前執(zhí)行作用域開(kāi)始,如果當(dāng)前作用域找不到,就會(huì)往上級(jí)作用域繼續(xù)查找,每次上升一級(jí)作用域,直至到頂級(jí)的全局作用域
- 不成功的RHS查詢(xún)會(huì)拋出Reference異常,而不成功的LHS查詢(xún)會(huì)自動(dòng)式地創(chuàng)建一個(gè)全局變量