墨言妹帶你細讀《你不知道的
JavaScript
》系列的世界,深入JavaScript
語言內部,弄清楚JavaScript
每一個零部件的用途,知其然更要知其所以然。
作用域是什麼
- 問題1:變數儲存在哪裡?
- 問題2:程式需要時如何找到它們?
1.1 編譯原理
通常,把 JavaScript
歸類為 “ 動態 ” 或 “ 解釋執行 ” 的語言,但是事實上它是一門 編譯語言,不提前編譯,編譯結果也不在分散式系統中進行移植。
JavaScript 引擎進行編譯的步驟和傳統的編譯語言非常相似,在某些環節比它要複雜。
傳統編譯語言,在執行之前的三個步驟,統稱為 “ 編譯 ” 。
-
分詞/詞法分析(
Tokenizing/Lexing
)將有字元組成的字串分解成(對程式語言來說)有意義的程式碼塊,這些程式碼塊被稱為詞法單元(
token
)。var a = 2; 複製程式碼
被分解成詞法單元:
var
、a
、=
、2
、;
。空格在該語言中有意義,則會被當做詞法單元,否則不是。 -
解析/語法分析(
Parsing
)將詞法單元流(陣列)轉換成一個由元素逐級巢狀所組成的代表了程式語法結構的 “ 抽象語法樹 ”(
Abstract Syntax Tree ,
AST
)。var a = 2; 複製程式碼
以上程式碼的抽象語法樹如下所示:
VariableDeclaration
頂級節點Identifer
子節點,值為a
AssignmentExpression
子節點NumericLiteral
子節點,值為2
-
程式碼生成
將
AST
轉換為可執程式碼的過程被稱為程式碼生成。這個過程與語言、目標平臺等相關。即通過某種方法,將
var a = 2 ;
的AST
轉化為一組機器指令,用來建立一個變數a
,並將值儲存在a
中。引擎,可以根據需要建立並儲存變數。
1.2 理解作用域
1.2.1 演員表
- 引擎,從頭到尾負責整個
JavaScript
程式的編譯及執行過程。 - 編譯器,負責語法分析及程式碼生成等髒活累活。
- 作用域,負責收集並維護由所有宣告的識別符號(變數)組成一系列查詢,並實施一套非常嚴格的規則,確定當前執行的程式碼對這些識別符號的訪問許可權。
1.2.2 對話
JavaScript
引擎是如何處理 JavaScript
程式碼的?
比如 var a = 2;
存在2個不同的宣告,變數的賦值操作會執行兩個動作。
- 遇到
var a
,在編譯階段,編譯器先詢問當前作用域,在作用域集合中是否存在變數a
,若不存在則宣告一個新變數名為a
;接著編譯器會為引擎生成執行時所需的程式碼,處理a = 2
這個賦值操作。 - 遇到 (
a = 2
),在執行階段,引擎執行時先詢問作用域,在作用域中查詢該變數a
,如果找到就將值2
賦值給變數a
,否則引擎就會舉手示意並丟擲一個異常。
1.2.3 編譯器有話說
-
如何理解引擎、編譯器、作用域的關係
- 程式碼先編譯後執行,當編譯器在編譯過程的第二步中生成了程式碼,引擎執行它時,會通過查詢變數
a
來判斷它是否已經宣告過。 - 查詢的過程由作用域進行協助,但是引擎執行怎樣的查詢,會影響最終的查詢結果。對
JavaScript
引擎的效能要求很高。
- 程式碼先編譯後執行,當編譯器在編譯過程的第二步中生成了程式碼,引擎執行它時,會通過查詢變數
-
引擎查詢的兩種方式:
RHS
和LHS
LHS
查詢(左側):找到變數的容器本身,然後對其賦值,即賦值操作的目標是誰。比如a = 2;
,為= 2
這個賦值操作找到一個目標。RHS
查詢(非左側):查詢某個變數的值,理解為retrieve his source value
,即誰是賦值操作的源頭。比如:console.log( a );
,需要獲取到變數a
的值,則對變數a
的RHS
查詢,並傳值給console.log(...)
。
function foo(a){
console.log( a ); //2
}
foo(2);
複製程式碼
上述程式碼共有1處 LHS
查詢,3處 RHS
查詢。
LHS
查詢有:- 隱式的
a = 2
中,在2
被當做引數傳遞給foo(...)
函式時,需要對引數a
進行LHS
查詢
- 隱式的
RHS
查詢有:- 最後一行
foo(...)
函式的呼叫需要對foo
進行RHS
查詢,意味著 “去找到foo
的值,並把它給我 ” ,並且(...)
意味著foo
的值需要被執行,因此它最好真的示意函式型別的值。 console.log( a );
中對a
進行RHS
查詢,並且將得到的值傳給了console.log(...)
。console.log(...)
本身對console
物件進行RHS
查詢,並且檢查得到的值中是否有一個叫作log
的方法。
- 最後一行
1.3 作用域巢狀
作用域是一套規則,用於確定在何處以及如何查詢變數(識別符號)。當一個塊或函式巢狀在另一個塊或函式中時,就發生了作用域的巢狀。
- 如果查詢的目的是對變數進行賦值,那麼就會使用
LHS
查詢;如果目的是獲取變數的值,就會使用RHS
查詢。 - 賦值操作符會導致
LHS
查詢。=
操作符或呼叫函式時傳入引數的操作都會導致關聯作用域的賦值操作。
遍歷巢狀作用域鏈的規則: 引擎從當前的執行作用域開始查詢變數,如果找不到,就向上一級繼續查詢。當抵達最外層的全域性作用域時,無論找到還是沒找到,查詢過程都會停止。
1.4 異常
為什麼區分 LHS
和 RHS
?變數還沒有宣告(在任何作用域中都無法找到該變數)的情況下,這兩種查詢的行為是不一樣的。
function foo(a){
console.log(a + b);
b = a;
}
foo(2);
複製程式碼
對一個 “未宣告 ” 的變數 b
進行 RHS
查詢時,在任何相關的作用域中都無法找到它。
ReferenceError
和作用域判別失敗相關,而 TypeError
則代表作用域判別成功了,但是對結果的操作是非法或不合理的。
RHS
查詢在作用域鏈中搜尋不到所需的變數,引擎會丟擲ReferenceError
異常。- 非嚴格模式下,
LHS
查詢在作用域鏈中搜尋不到所需的變數,全域性作用域中會建立一個具有該名稱的變數並返還給引擎。 - 嚴格模式下(
ES5
開始,禁止自動或隱式地建立全域性變數),LHS
查詢失敗並不會建立並返回一個全域性變數,引擎會丟擲同RHS
查詢失敗時類似的ReferenceError
異常。 - 在
RHS
查詢成功情況下,對變數進行不合理的操作,引擎會丟擲TypeError
異常。(比如對非函式型別的值進行函式呼叫,或者引用null
或undefined
型別的值中的屬性)。
最後, 書讀百遍其義自見,抱著以教為學的初衷,不斷反思、刻意練習,若對你有幫助,請點個贊,謝謝您的支援與指教。
參考文獻: 木易楊部落格
歷史文章: 【譯】30 Seconds of ES6 (一)