第1章:作用域是什么
- 我們通過
var
聲明變量時,是否考慮過這些問題:- 這些變量都存儲在哪里?
- 程序用到它們時,又是怎么找到它們的?
- 而答案就是:不僅僅是JavaScript,任何編程語言都會設計一套良好的規則來存取變量,而這套規則就叫做 作用域。
1.1 編譯原理
- 雖然和靜態語言(比如Java)不同,JavaScript是“解釋性”的動態語言。
- 但實際上,JavaScript代碼在運行之前也是需要編譯的,并且JavaScript引擎編譯的步驟,和傳統的編譯語言非常相似,大致有以下三大步驟:
第1步:分詞/詞法分析(Tokenizing/Lexing)
- 任何
.js
文件在解析前,對于JS引擎而言都是一大段文本,不能直接運行。所以當務之急,就是將文本字符串“大卸八塊”般的進行分解。 - 詞法分析就是 將文本內容分解成有意義的詞法字符串(token) 。
- 比如
var a = 2;
最終會分解成詞法字符串數組,得到 [var
、a
、=
、2
、;
],而多余的空格則是無意義的。
第2步:解析/語法分析(Parsing)
- 語法分析則是 將詞法字符串數組轉換成 “抽象語法樹”(Abstract Syntax Tree,AST)
- 比如代碼
var a = 2;
會生成以下具有層次結構的對象/*變量聲明的對象*/ VariableDeclaration : { /*變量名為 a*/ Identifier : a, /*變量賦值表達式*/ AssignmentExpression : { /*數值類型為 2*/ NumericLiteral : 2 } }
第3步:代碼生成
- 最后一步就是生成代碼, 將AST轉換為可執行的機器指令 。
- 比如代碼
var a = 2;
會創建一個變量a
,并為其分配內存,然后將值2
存進這個變量。
1.2 理解作用域
原書將引擎、編譯器以及作用域模擬成三個演員,用來說明在執行一段代碼時,三者分別負責的工作。但我稍微做一些改動,將作用域比喻成一個記錄清單。
- 執行JS代碼依賴三個東西:
-
引擎
:負責JS代碼的編譯和執行 -
編譯器
:在引擎工作前,負責語法分析和代碼生成 -
作用域
:一個具有嚴格的規則,專門負責收集并維護所有變量的清單列表,通過它來存取變量
-
- 閱讀代碼
var a = 2;
其實訪問了兩次作用域,一個是 在編譯器編譯時檢查變量聲明,一個是 引擎運行時檢查使用:- 如上面所說的,第1步編譯器會進行詞法分析,第2步將詞法單元解析成一個樹結構的對象;
- 在第3步生成代碼時,編譯器會去查找作用域,檢查 是否存在同名的變量,如果沒有則聲明一個新的變量并賦值 ;
- 最后引擎運行代碼時,會再次通過作用域 檢查 是否存在同名的變量,如果有則直接 使用,沒有則繼續向上查找
- 引擎執行代碼到作用域查找變量,分為兩種類型:RHS查詢 和 LHS查詢:
- “L(left)”和“R(right)”分別代表變量處于表達式的左邊還是右邊;
- RHS查詢就是查找變量,可理解成retrieve his source value(找到它源值)。比如
console.log(a)
就是RHS查詢,找到變量a
的值傳遞給console.log()
; - LHS查詢則是查找變量的容器對其進行賦值。比如
var a = 2;
就是LHS查詢,找到變量a
并為它賦值= 2
;
- 我們嘗試用RHS查詢和LHS查詢的思維來閱讀JS代碼:
我們都知道function foo(a){ console.log(a); } foo(2);
function
聲明函數的方式等同于,聲明一個變量并為其賦值一個執行方法體:var foo = function(a){ console.log(a); } foo(2);
-
var foo = function()
這是一個LHS查詢:聲明foo
變量并為其賦值一個方法; -
foo(2)
屬于RHS查詢:找到foo
變量的值并執行它 - 進到
foo
方法體中,實際上這里隱藏了一句代碼a = 2;
將傳遞的值賦值給形參 -
console.log(a)
是RHS查詢:找到a
的值,傳遞給console.log(...)
- 值得一提的是,
console.log()
本身也屬于RHS查詢,會去找尋log()
方法的引用并執行它
-
1.3 作用域嵌套
- 不管是RHS查詢還是LHS查詢都從當前作用域開始,如果當前作用域無法找到變量時,引擎會轉移到外層作用域中繼續查找,直至轉移到最頂層的作用域,也就是全局作用域。
- 舉例:
在function foo(a){ console.log(a + b); } var b = 2; foo(2);
foo
方法體中,變量b
在foo
的作用域中找不到,將會到外層的全局作用域查找,最后輸出4
1.4 異常
- 之所以 區分RHS和LHS,是因為當查找到未聲明的變量時,這兩種查詢的行為是不一樣的:
- 如前文提到的,LHS查詢失敗時會在全局作用域創建一個同名的變量;
- 而RHS查詢失敗時,則會拋出 ReferenceError異常;另一種情況是,查找到了變量,但是嘗試對這個變量的值做不合理的操作(比如對一個非函數的變量進行調用),則拋出TypeError異常
- 總而言之,RererenceError異常是作用域判別失敗相關的, TypeError異常 則代表作用域判別成功了,但對結果的操作是非法或不合理的
1.5 小結
- 作用域是一套存取變量的規則;
- 在代碼執行前,會先由編譯器進行編譯,JavaScript引擎在執行代碼時會進行LHS查詢和RHS查詢:
-
LHS查詢是對變量進行賦值,其中
=
操作符或者調用函數時傳參的操作,都會導致相關作用域的賦值操作; - RHS查詢是對變量的值進行查找;
-
LHS查詢是對變量進行賦值,其中
- LHS和RHS查詢都會從當前執行作用域開始,如果當前作用域找不到,就會往上級作用域繼續查找,每次上升一級作用域,直至到頂級的全局作用域
- 不成功的RHS查詢會拋出Reference異常,而不成功的LHS查詢會自動式地創建一個全局變量