六角學院 JS 核心筆記 (五)【執行環境與作用域】- 範圍鍊 (Scope Chain) 與 詞彙環境 (Lexical Environment)

Scope

Scope 是負責維護變數名稱 (Identifier) 的清單。編譯器或 JavaScript 引擎在編譯期間和執行期間查找變數時會有一套規則。主要有三類可以討論:

  1. Global
  2. 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) 的數據結構。

  1. 編譯時期:會先存放好變數或是函式名稱 (identifier) 和初始內容、變量 (variables or functions)。內容可能是一般的值或是 reference 指向函式物件或陣列物件。
  2. 執行期間:會將初始數據複製一份,然後依據是否重新指派變數或函式的內容,來進行更新

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
2
3
4
5
6
7
8
9
10
11
var x = 1;

function fn1(){
console.log(x); // 輸出結果:1
}
function fn2(){
var x = 2;
fn1();
}

fn2();

執行期間,執行環境產生後會有類似下面這樣的 Lexical Environment 產生。這是 pseudocode (虛擬碼) 不是真的長這樣。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// environment of the global context
// 全域執行環境的詞彙環境

globalEnvironment = {

environmentRecord: {
// built-ins 內建:
Object: function,
Array: function,
// etc ...

// our bindings 自定義:
x: 1,
fn1: <ref. to fn1 function>,
fn2: <ref. to fn2 function>
},

outer: null // no parent environment 沒有上層函式
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// environment of the "fn1" function
// fn1 執行環境的詞彙環境
fn1Environment = {
environmentRecord: {
},
outer: globalEnvironment
};

// environment of the "fn2" function
// fn2 執行環境的詞彙環境
fn2Environment = {
environmentRecord: {
x: 2
},
outer: globalEnvironment
};

程式第 11 行呼叫 fn2() … 一直到程式執行第 4 行 console.log(x) 時,先在 fn1EnvironmentenvironmentRecord 裡面查找 x 變數,發現找不到後,決定求助於外層函式,由 outer: globalEnvironment 發現外層函式是 globalEnvironment。在 globalEnvironmentenvironmentRecord 裡面查找 x 變數,找到 x: 1,因此順利輸出到螢幕上。

上述的 console.log(x) 要查找的變數不在指定動作 = 的左邊,也就是執行 RHS 查找動作。如果有錯誤會丟出 Uncaught ReferrenceError 的訊息。如果對於 LHS、RHS 不了解,可以參考:
六角學院 JS 核心筆記 (二)【執行環境與作用域】- 執行的錯誤情境 LHS、RHS

舉例二

調整一下 fn1 函式宣告的位置:

1
2
3
4
5
6
7
8
9
10
11
12
var x = 1;

function fn2(){
var x = 2;
function fn1(){
console.log(x); // 輸出結果:2
}

fn1();
}

fn2();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// environment of the global context
// 全域執行環境的詞彙環境

globalEnvironment = {
environmentRecord: {
// built-ins 內建:
Object: function,
Array: function,
// etc ...

// our bindings 自定義:
x: 1,
fn2: <ref. to fn2 function>
},

outer: null // no parent environment 沒有上層函式
};
1
2
3
4
5
6
7
8
9
// environment of the "fn2" function
// fn2 執行環境的詞彙環境
fn2Environment = {
environmentRecord: {
x: 2
fn1: <ref. to fn1 function>
},
outer: globalEnvironment // 上層是全域詞彙環境
};
1
2
3
4
5
6
7
// environment of the "fn1" function
// fn1 執行環境的詞彙環境
fn1Environment = {
environmentRecord: {
},
outer: fn2Environment // 上層是 fn2 詞彙環境
};

這時一樣呼叫 fn2() … 一直到程式執行第 6 行 console.log(x) 時,先在 fn1EnvironmentenvironmentRecord 裡面查找 x 變數,發現找不到後,決定求助於外層函式,由 outer: fn2Environment 發現外層函式是 fn2Environment。在 fn2EnvironmentenvironmentRecord 裡面查找 x 變數,找到 x: 2,因此順利輸出到螢幕上。

參考資料:
Understanding Scope and Scope Chain in JavaScript
ECMAScript:8.1 Lexical Environments