細讀《你不知道的JavaScript·上卷》1-1 作用域是什麼?

墨言妹發表於2019-01-13

細讀《你不知道的JavaScript·上卷》1-1 作用域是什麼?

墨言妹帶你細讀《你不知道的 JavaScript 》系列的世界,深入 JavaScript 語言內部,弄清楚 JavaScript 每一個零部件的用途,知其然更要知其所以然。

作用域是什麼

  • 問題1:變數儲存在哪裡?
  • 問題2:程式需要時如何找到它們?

1.1 編譯原理

通常,把 JavaScript 歸類為 “ 動態 ” 或 “ 解釋執行 ” 的語言,但是事實上它是一門 編譯語言,不提前編譯,編譯結果也不在分散式系統中進行移植。

JavaScript 引擎進行編譯的步驟和傳統的編譯語言非常相似,在某些環節比它要複雜。

傳統編譯語言,在執行之前的三個步驟,統稱為 “ 編譯 ” 。

  • 分詞/詞法分析( Tokenizing/Lexing

    將有字元組成的字串分解成(對程式語言來說)有意義的程式碼塊,這些程式碼塊被稱為詞法單元( token )。

    var a = 2;
    複製程式碼

    被分解成詞法單元:vara=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 引擎的效能要求很高。
  • 引擎查詢的兩種方式: RHSLHS

    • LHS 查詢(左側):找到變數的容器本身,然後對其賦值,即賦值操作的目標是誰。比如 a = 2; ,為 = 2 這個賦值操作找到一個目標。
    • RHS查詢(非左側):查詢某個變數的值,理解為 retrieve his source value ,即誰是賦值操作的源頭。比如: console.log( a ); ,需要獲取到變數 a 的值,則對變數 aRHS 查詢,並傳值給 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 異常

為什麼區分 LHSRHS ?變數還沒有宣告(在任何作用域中都無法找到該變數)的情況下,這兩種查詢的行為是不一樣的。

function foo(a){
	console.log(a + b);
	b = a;
}
foo(2);
複製程式碼

對一個 “未宣告 ” 的變數 b 進行 RHS 查詢時,在任何相關的作用域中都無法找到它。

ReferenceError 和作用域判別失敗相關,而 TypeError 則代表作用域判別成功了,但是對結果的操作是非法或不合理的。

  • RHS 查詢在作用域鏈中搜尋不到所需的變數,引擎會丟擲 ReferenceError 異常。
  • 非嚴格模式下,LHS 查詢在作用域鏈中搜尋不到所需的變數,全域性作用域中會建立一個具有該名稱的變數並返還給引擎。
  • 嚴格模式下( ES5 開始,禁止自動或隱式地建立全域性變數), LHS 查詢失敗並不會建立並返回一個全域性變數,引擎會丟擲同 RHS 查詢失敗時類似的 ReferenceError 異常。
  • RHS 查詢成功情況下,對變數進行不合理的操作,引擎會丟擲 TypeError 異常。(比如對非函式型別的值進行函式呼叫,或者引用 nullundefined 型別的值中的屬性)。

最後, 書讀百遍其義自見,抱著以教為學的初衷,不斷反思、刻意練習,若對你有幫助,請點個贊,謝謝您的支援與指教。

參考文獻: 木易楊部落格

歷史文章: 【譯】30 Seconds of ES6 (一)

相關文章