[ 筆記 ] JavaScript 進階 05 - Execution Context - Scope Chain


Posted by krebikshaw on 2020-09-26

Execution Context 執行環境

JavaScript 所有的程式碼都要在 執行環境 Execution Context 中執行,我們要搞懂 JavaScript 的行為之前,需要先了解 Execution Context 是如何運作的。

當我們要準備運行 JavaScript 程式碼時,第一個 Execution Context 就是全域物件的 Execution Context,簡稱 Global EC。

之後每建立一個新的執行環境時,這些新的 EC 就會以 call stack 的方式進行堆疊。(如果不知道 call stack 是什麼,可參考
[ 筆記 ] JavaScript 進階 - Event Loop

Execution Context 的內容有哪些?

你可以想像成 JavaScript 的物件,裡面的內容會有 VO 跟 scopeChain 跟 this:

EC: {
    Variable Object (VO): {
        arguments: {
            ... 
        },
        FD: <reference to function>
        <variable>: <value>,
    },
    scopeChain: [...],
    this: ...
}

VO / AO 的部分在
[ 筆記 ] JavaScript 進階 - Execution Context - Variable Object
有提到了,今天的主要重點是 scopeChain

Scope Chain 作用域鍊

Scope Chain 是系統用來尋找變數數值的路徑,舉例來說:

var a = 10;
function test() {
  console.log(a)  // 10
}

test()

系統在 test() 這個 function 裡面找不到 a 的數值是多少,所以就到 global 去找,而這個尋找數值的路徑,就是所謂的 Scope Chain

要搞懂 JavaScript 尋找變數數值的路徑之前,要先了解兩個東西:

  • [[scope]]
  • scopeChain

這兩個東西看起來差沒多少,我們要注意的是這兩個東西 產生的時機點 以及 它初始化時的數據為何

[[scope]]

  • 產生的時機:function 被「宣告」的時候
    • 每一個新的 function 被宣告的同時,就會建立一個屬於自己的 [[scope]] 屬性
  • 初始化的數據:function 所處的執行環境,底下的 scopeChain

scopeChain

  • 產生的時機:function 被「呼叫」的時候
  • 初始化的數據:function 自己的 AO 再加上自己的 [[scope]] 屬性
    • 進入新的 Execution Context 時,就會初始化 scopeChain 為 function EC.AO 再加上它自己的 [[scope]] 屬性

OK,看不懂很正常,我們試著用正常人類的語言重新解釋一遍:

甲、乙、丙、丁、戊五位同學是同班同學,有一天老師出了一份作業,要請五位同學接力畫出一幅圖畫,主題是「人」。於是甲、乙、丙、丁、戊五位同學就決定以輪流的方式,甲先畫好,再給乙畫,乙畫好後再給丙畫,以此類推。那為了要能分辨每位同學畫的分別是什麼,所以每個人都要自己寫一份自己的「繪圖心得」。並且在圖畫的傳遞過程中,把這個傳遞的訊息紀錄在圖畫背面的「傳遞日誌」上,比如說:「畫從甲同學手上交到乙同學手上,乙同學就要在圖畫背面的傳遞日誌寫上 甲 -> 乙」。

於是甲同學先開始畫了,甲同學畫好了人物的眼睛跟鼻子,在自己的「繪圖心得」裡寫上眼睛、鼻子,並把畫交給了乙同學,乙同學拿到圖畫後,在圖畫背面的「傳遞日誌」寫上 甲 -> 乙,並且開始在圖畫上面畫上耳朵跟眉毛,完成後在自己的「繪圖心得」裡寫上耳朵、眉毛,並把畫交給了丙同學,丙同學拿到圖畫後,在圖畫背面的「傳遞日誌」寫上 乙 -> 丙,並開始畫畫......(一路傳遞到戊同學手上)。

最後作業完成了,五位同學把圖畫交給老師,老師看了一眼之後非常震驚,畫得太好了!他請五位同學也把自己的「繪圖心得」交上來,打算帶回家慢慢欣賞。

老師在家裡拿出圖畫後,覺得眼睛的部分實在是畫的太傳神了,他很想知道眼睛到底是誰畫的,於是老師看了一下圖畫背面的「傳遞日誌」,上面寫著:「甲 -> 乙 -> 丙 -> 丁 -> 戊」。最後一位是戊同學,於是老師拿出戊同學的「繪圖心得」,裡面沒有寫到眼睛,代表眼睛不是戊同學畫的,再看一下傳給戊同學的是丁同學,於是老師拿出丁同學的「繪圖心得」,裡面也沒有寫到眼睛,代表眼睛也不是丁同學畫的,老師依照傳遞日誌的傳遞順序,一路找回去之後,發現在甲同學的「繪圖心得」有寫到眼睛,終於知道原來眼睛是甲同學畫的呀!

這一連串的尋找過程,就是循著「傳遞日誌」當中紀錄的訊息,一路從最後一位同學開始尋找,找不到就再找前一位,直到找到為止,這就是這份「傳遞日誌」神奇的地方。

========================故事還沒結束========================

我們把時間倒回到甲同學畫完的時候:

甲同學把圖畫交給乙同學,乙除了拿到了圖畫之外,也同時拿到了圖畫背後的「傳遞日誌」,這份傳遞日誌就是乙同學的 [[scope]],上面寫著:「甲」。當乙同學把自己的部分畫好之後,在圖畫背後的「傳遞日誌」寫上「甲 -> 乙」,這個「甲 -> 乙」就是乙同學的 scopeChain,記錄的是甲同學畫的內容加上自己畫上的內容。而當乙同學再把「傳遞日誌」交給丙同學時,丙同學收到的 [[scope]],就會是「甲 -> 乙」(乙同學的 scopeChain),丙同學把自己的部分畫好之後,在圖畫背後的「傳遞日誌」寫上「甲 -> 乙 -> 丙」,這就會變成丙同學的 scopeChain 記錄的是甲同學畫的內容加上乙同學畫的內容再加上自己畫上的內容。

所以我們可以發現,誰收到圖畫的時候,就會同時收到前面的人傳遞過來的 [[scope]],接著在把自己畫的內容也加進去,成為了自己的 scopeChain,這個 scopeChain 就成為了要傳遞給下一個人的「傳遞日誌」。

所以一路傳到戊同學之後,戊同學收到的 [[scope]] 就會紀錄著前面所有傳遞過程中每位同學所畫的內容,一但他想知道任何部位是誰畫的,都能順著 [[scope]] 一路尋找回去。

結合剛剛所講的部分:

[[scope]]

  • 產生的時機:function 被宣告的時候
    • 甲同學宣告 乙同學 這個 function 的時候
    • 就會同時建立 乙同學.[[scope]] 這個屬性
  • 初始化的數據:function 所處的執行環境,底下的 scopeChain
    • [[scope]] 的數值,就會是甲同學的 scopeChain 的數值(甲同學畫的內容)

scopeChain

  • 產生的時機:function 被呼叫的時候
    • 乙同學() 被呼叫的時候,就會建立自己的 scopeChain
  • 初始化的數據:function 自己的 AO 再加上自己的 [[scope]] 屬性
    • 乙同學() 把自己畫的內容加進去圖畫裡面,產生自己的 scopeChain(甲同學畫的內容加上自己畫的內容)

當乙同學要把圖畫傳遞給給丙同學時:

  • 宣告 丙同學 這個 function
  • 同時建立 丙同學.[[scope]] 這個屬性
  • [[scope]] 的數值,就會是乙同學的 scopeChain 的數值(甲同學畫的內容加上自己畫的內容)

所以回到 JavaScript 的世界,Scope Chain 傳遞的訊息,就是「找尋變數的路徑」,每個被宣告的 function 都會收到傳遞下來的這份 [[scope]] 屬性,把這份 [[scope]] 屬性加上自己的 AO 之後,就會成為是自己的 scopeChain,這份 scopeChain 又可以再接著傳遞下去。

拿程式碼來看:

var a = 1;
function test() {
  var b = 2;
  function inner() {
    var c = 3;
    console.log(b);
    console.log(a);
  }
  inner()
}

test()

一開始在 global EC 建立時,會先初始化 global EC 的 VO 及 global EC 的 scopeChain

globalEC: {
  VO: {
    a: undefined,
    test: function
  },
  scopeChain: [globalEC.VO]
}

// 因為有宣告 test 所以會建立 test 的 [[scope]] 屬性
test.[[scope]] = globalEC.scopeChain

接著開始執行程式:

  1. a = 1 ,將 globalEC.VO.a 更改為 1
  2. 呼叫 test()

test() 被呼叫時,testEC 被建立,初始化 testEC.AO 及 testEC 的 copeChain

testEC: {
  AO: {
    b: undefined,
    inner: function
  },
  scopeChain: [testEC.AO, test.[[scope]]]
}
// 因為有宣告 inner 所以會建立 inner 的 [[scope]] 屬性
inner.[[scope]] = testEC.scopeChain

globalEC: {
  VO: {
    a: 1,
    test: function
  },
  scopeChain: [globalEC.VO]
}
test.[[scope]] = globalEC.scopeChain

接著開始執行 test() 函式:

  1. b = 2 ,將 testEC.AO.b 更改為 2
  2. 呼叫 inner()

inner() 被呼叫時,innerEC 被建立,初始化 innerEC.AO 及 innerEC 的 copeChain

innerEC: {
  AO: {
    c: undefined
  },
  scopeChain: [innerEC.AO, inner.[[scope]]]
}


testEC: {
  AO: {
    b: undefined,
    inner: function
  },
  scopeChain: [testEC.AO, test.[[scope]]]
}
inner.[[scope]] = testEC.scopeChain

globalEC: {
  VO: {
    a: 1,
    test: function
  },
  scopeChain: [globalEC.VO]
}
test.[[scope]] = globalEC.scopeChain

我們可以來分析最後產生的這個 innerEC.scopeChain 它把裡面所有的內容都展開之後,就會變成

innerEC.scopeChain = [innerEC.AO, testEC.AO, globalEC.VO]

這段 scopeChain 就成為最後 console.log(b) 及 console.log(a) 尋找數值時的路徑。

  1. 在 innerEC.AO 找找看有沒有 b
  2. 沒有找到就去 testEC.AO 找,找到 b = 2 把它印出來
  3. 在 innerEC.AO 找找看有沒有 c
  4. 沒有找到就去 testEC.AO 找
  5. 沒有找到就去 globalEC.VO 找,找到 a = 1 把它印出來

這就是 JavaScript 當中 Scope Chain 的運作方式了

知道了 Scope Chain 的運作方式之後,我們就可以接下去了解 JavaScript 當中的 Closure 屬性原理是什麼了,Closure 將會在下一篇文章做說明。


#Scope Chain







Related Posts

Day 89

Day 89

service worker

service worker

【Day03】用爬蟲抓影片播放清單的所有影片連結

【Day03】用爬蟲抓影片播放清單的所有影片連結


Comments