六角學院 JS 核心筆記 (五)【執行環境與作用域】- 範圍鍊 (Scope Chain) 與 詞彙環境 (Lexical Environment)
Scope
Scope 是負責維護變數名稱 (Identifier) 的清單。編譯器或 JavaScript 引擎在編譯期間和執行期間查找變數時會有一套規則。主要有三類可以討論:
- Global
- Local
- Function
- Block (ES6)
但是,目前如果將 Block Scope 加進來會有點太複雜,所以先不談。
範圍鍊 Scope Chain
當函式的本身沒有宣告該變數時,函式就會一層一層向外層 / 上層來做尋找,而這一連串就是範圍鍊。
它尋找的過程與執行堆疊沒有關連。
範圍鍊是用來尋找變數的值。原型鍊則是取用物件的屬性或方法。兩者不一樣,不要搞混。
如果有確實搞懂何謂 語法作用域 (Lexical scope) 和 執行環境 (Execution context),應該可以輕易理解範圍鍊的觀念,還不是很請楚的人可以參考我的前兩篇筆記:
六角學院 JS 核心筆記 (三)【執行環境與作用域】- 語法作用域 (Lexical scope)
六角學院 JS 核心筆記 (四)【執行環境與作用域】- 執行環境與執行堆疊
如下圖中所示,fn1()
和 fn2()
在查找變數時,如果內部找不到,都是直接向上層查找。
參考資料:
六角學院:JavaScript 核心篇 - 邁向達人之路
詞彙環境 Lexical Environment
簡介
在進入下一章節之前,我想提一個我覺得非常重要的東西,可以幫助執行環境與作用域的理解和觀念上的連接,就是詞彙環境 (Lexical Environment)。
我們之前了解到 JavaScript 的語法作用域 (Lexical scope) 類型是静態作用域 (Static Scoping)。也就是函式在執行之前 (定義的時候) 就已經確定了它的作用域,確定了它能使用的變數。因此開發者才能從程式碼中、程式碼的物理位置直接觀察到變數的作用域。
人可以直接用眼睛觀察,但是程式不行。因此,程式在編譯時期會確定好語法作用域 (Lexical scope) 的範圍,並且產生一個叫做詞彙環境 (Lexical Environment) 的數據結構。
- 編譯時期:會先存放好變數或是函式的名稱 (identifier) 和初始內容、變量 (variables or functions)。內容可能是一般的值或是 reference 指向函式物件或陣列物件。
- 執行期間:會將初始數據複製一份,然後依據是否重新指派變數或函式的內容,來進行更新。
According to ECMAScript specification 262 (8.1):
A Lexical Environment is a specification type used to define the association of Identifiers to specific variables and functions based upon the lexical nesting structure of ECMAScript code. A Lexical Environment consists of an Environment Record and a possibly null reference to an outer Lexical Environment.
而範圍鍊就是依照 Lexical Environment 這個數據來查找變數。
其實 Lexical Environment 還牽涉到了滿多其他的觀念,但是這邊先了解上述的說明就好。
舉例一
我們將範圍鍊的例子拿來說明:
1 | var x = 1; |
執行期間,執行環境產生後會有類似下面這樣的 Lexical Environment 產生。這是 pseudocode (虛擬碼) 不是真的長這樣。
1 | // environment of the global context |
1 | // environment of the "fn1" function |
程式第 11 行呼叫 fn2()
… 一直到程式執行第 4 行 console.log(x)
時,先在 fn1Environment
的 environmentRecord
裡面查找 x
變數,發現找不到後,決定求助於外層函式,由 outer: globalEnvironment
發現外層函式是 globalEnvironment
。在 globalEnvironment
的 environmentRecord
裡面查找 x
變數,找到 x: 1
,因此順利輸出到螢幕上。
上述的 console.log(x)
要查找的變數不在指定動作 =
的左邊,也就是執行 RHS 查找動作。如果有錯誤會丟出 Uncaught ReferrenceError 的訊息。如果對於 LHS、RHS 不了解,可以參考:
六角學院 JS 核心筆記 (二)【執行環境與作用域】- 執行的錯誤情境 LHS、RHS
舉例二
調整一下 fn1
函式宣告的位置:
1 | var x = 1; |
1 | // environment of the global context |
1 | // environment of the "fn2" function |
1 | // environment of the "fn1" function |
這時一樣呼叫 fn2()
… 一直到程式執行第 6 行 console.log(x)
時,先在 fn1Environment
的 environmentRecord
裡面查找 x
變數,發現找不到後,決定求助於外層函式,由 outer: fn2Environment
發現外層函式是 fn2Environment
。在 fn2Environment
的 environmentRecord
裡面查找 x
變數,找到 x: 2
,因此順利輸出到螢幕上。
參考資料:
Understanding Scope and Scope Chain in JavaScript
ECMAScript:8.1 Lexical Environments