[JavaScriptWeird]No.63 從 ECMAScript 看作用域

前言

之前我們已經在 hoisting 嘗試過從 ECMAScript 文件內找出其原理,然後假裝自己是 JavaScript 引擎,但那個時候我們描述的不夠完整,因此這一小節要補足剩餘的部分。閉包其實也與作用域、範圍鏈脫離不了關係,因此在繼續深入了解閉包之前,還是要先對這兩者之間有更多認識才行。

作用域 & 範圍鏈

本節使用文件

於 hoisting 的章節時,我們有了執行環境 (EC)、變數物件 (VO) 的概念,本小節就繼續從這邊紀錄下去。

Every execution context has associated with it a scope chain.

  • 每個執行環境都有範圍鏈 (Scope Chain)

    When control enters an execution context, a scope chain is created and populated with an initial set of objects

  • 大概的意思就是,當進入執行環境時,範圍鏈就會被建立

    When control enters an execution context, the scope chain is created and initialised, variable instantiation is performed, and the this value is determined

  • 當進入執行環境時,範圍鍊被建立且初始化,變數也被初始化並且確定其值

接著跳到 Function Code 的段落

The scope chain is initialised to contain the activation object followed by the objects in the scope chain stored in the [[Scope]] property of the Function object.

  • 當進入執行環境時,範圍鏈的初始化將包含 activation object ,以及函式的 [[Scope]] 屬性
  • 當進入執行環境時,如有宣告函式,則將該函式的 [[Scope]] 屬性賦值為自身的範圍鏈

接著了解什麼是 activation object ,以下稱為 AO

When control enters an execution context for function code, an object called the activation object is created and associated with the execution context. The activation object is initialised with a property with name arguments and attributes { DontDelete }. The initial value of this property is the arguments object described below.

The activation object is then used as the variable object for the purposes of variable instantiation

  • 大致上的意思就是,當我們進入一個執行環境時,會產生一個 AO (之前都說 VO ,但實際上是AO),而這個 AO 其實跟 VO 只有一些細微的差異,而大部分的行為都是相同的。
  • 只有全域執行環境有 VO

接下來我們一樣假裝自己是 JS 引擎,分析一段簡單的程式碼:

1
2
3
4
5
6
7
8
9
10
11
var a = 1;  
function test() {
var b = 2;
function inner(){
var c = 3;
console.log(b);
console.log(a);
}
inner();
}
test();

  • 創造全域執行環境,建立變數物件,並且初始化範圍鏈

建立變數物件的時候,因為有宣告 test 函式,所以 test.[[Scope]] 被賦值為 自身執行環境的 scopeChain ,即為 globalEC.scopeChain

而初始化範圍鏈的時候,因為本身並不是函式,所以 ScopeChain 並沒有包含 [[Scope]] 屬性,僅有 globalEC.VO

整理過後可以得到下圖,全域執行環境的範圍鏈就是自己的 VO 。

創造階段結束後接著是執行階段,開始逐行執行程式碼。

  • 1260 行將全域變數 a 賦值為 1
  • 1261~1269 跳過
  • 1270 呼叫 test() ,創造並進入另一個執行環境

目前的狀況是這樣的

test () 內發生的事

創造執行環境階段

  • 建立 AO ,並且初始化範圍鏈

testEC.scopeChain 內的東西就是 testEC.AO 以及 testEC.[[Scope]] 屬性,透過代換後,可以發現 testEC.[[Scope]] 屬性就是 globalEC.scopeChain ,更進一步代換就是 globalEC.VO

接著逐行運行程式碼

  • 1262 行將變數 b 賦值為 2
  • 1263~1267跳過
  • 1268 呼叫 inner() ,創造並進入另一個執行環境

inner () 內發生的事

創造執行環境階段

  • 建立 AO ,並且初始化範圍鏈

如同在 test() 內發生的事一樣, innerEC 的 ScopeChain 會被初始化,裡面會有 innerEC.AOinner.[[Scope]] ,接著可以透過不斷的代換,得知其實 innerEC 的 ScopeChain 會按照順序去找 innerEC.AOtestEC.AOglobalEC.VO 內的東西。

接著逐行運行程式碼

  • 1264 行將變數 c 賦值為 3
  • 1265 行印出變數 b ,但自身 AO 內找不到,透過自身 ScopeChain 向 testEC.scopeChain 尋找,於 testEC.AO 內找到變數 b 為 2
  • 1266 行印出變數 a ,但自身 AO 內找不到,且 testEC.AO 內也找不到,因此繼續往 globalEC.scopeChain 找,最後在 globalEC.VO 找到變數 a 為 1
  • inner 函式結束,移出執行堆
  • 回到 test 函式繼續執行
  • test 函式結束,移出執行堆
  • 回到全域執行環境繼續執行
  • 程式碼執行完畢,全域執行環境移出執行堆

透過這樣的方式可以更了解作用域以及範圍鏈,也明白範圍鏈在 JavaScript 中是如何一層一層的往外找到相應的變數。

而我們模仿 JS 引擎的這個行為,除了可以幫助我們了解 hoisting 以及 範圍鏈之外,還能夠幫助理解閉包的行為,下一節我們將繼續使用這樣的方式來解析閉包。

0%