在談 Hoisting 之前,我們先測試看看以下程式
1 | console.log(a); // 報錯: a is not defined |
那換種方式呢?
1 | var a = 1; |
a 居然不是 1
而是 undefined
!為什麼會這樣呢?接下來會從 ECMAScript(ECMAScript 是 JavaScript 的標準)第三版(ES3)來了解一下 JS 引擎最初是如何執行程式的。
這邊要特別注意一下,var
跟 let
const
的 hoisting 狀況不太一樣,這邊指的都是 var
。
執行模型
JS 在執行程式前的編譯階段時,會先將整份檔案視為一個 Global Execution Context ,中文名是全域執行環境,簡稱 Global EC,並把 Global EC 放入 stack 中。
接下來開始依序從第一行開始掃,在全域(Global)的環境碰到變數時,會先將變數初始值設成 undefined
後放入 Variable Object(以下簡稱 VO)。碰到函式宣告時,也會將函式放入 VO,key 為函式名,value 為 function,如果函式宣告時包含參數,參數會放進該函式類似 VO 的 Activation Object(以下簡稱 AO),並將函式視為一個 Execution Context,push 進 stack 中。
進入函式的 EC 時,一樣從函式第一行開始掃,如果發現有宣告變數,就會放入該函式 AO,一樣將變數初始值設成 undefined
。以底下例子為例:
1 | var foo = "bar"; |
步驟如下
- 建立 global EC 並 push 進 stack
- 第一行宣告變數 foo,放入 global EC 的 VO,並給予初始值 undefined
- 第二行宣告變數 a,放入 global EC 的 VO,並給予初始值 undefined
- 第三行宣告函式 bar,放入 global EC 的 VO,給予初始值 function。建立 bar EC 並 push 進 stack。發現宣告參數 a,將參數 a 放入 bar EC 的 AO,給予初始值 undefined
- 第四行沒有發現宣告,不做事
- 第五行宣告變數 a,但發現 a 已經存在 bar EC 的 AO 裡,因此不做事
- 第六行沒有宣告,不做事
- 第七行沒有宣告,不做事
- 第八行沒有宣告,不做事
- 第九行沒有宣告,不做事
- 第十行呼叫函式 bar,且參數是 4,更改 bar EC 的 AO,a 從 undefined 變成 4
編譯後的成果類似下圖:
1 | // 模擬 VO 及 AO |
1 | 模擬 stack 圖 |
特別注意以上都是編譯的結果,程式都還沒執行,程式開始執行時,實際步驟類似如下
1 | Line 1: 進入 Global scope,找 Global VO 是否有變數 voo ,有,將 "bar" 賦值給 Global VO 的變數 foo |
程式執行後,會變成
1 | // 執行後 VO,註解是編譯後的狀態到執行後的演變 |
因此執行過後結果是:
1 | var foo = "bar"; |
如果第五行跟第七行對調,這時候答案就會變了
1 | var foo = "bar"; |
了解執行模型後,回過頭來看剛開始的題目,其實就可以很清楚的了解,為什麼 a 是 undefined
而不是報錯 a is not defined。
優先順序
從上面可以知道,var
宣告的變數、函式及函式內的參數等都會被提升,那當今天這三個都同名時,誰會優先被提升呢?
1 | function test(a) { |
所以優先順序是:==函式 > 參數 > var 宣告的變數==。而當今天有兩個同名的函式時,後者會覆蓋前者
1 | function test() { |
練習
1 | var a = 1; |
答案在底下,請先練習
======= 防雷線 =========
- undefined
- 7
- 8
- 30
- 30
- 1
- 70
- 報錯 b is not defined
TDZ
var
相關的 hoisting 都了解後,現在可以來了解前面說跟 var
有點不一樣的 let
const
。先從範例來看:
1 | let a = 1; |
跑完會報錯,感覺很像是 let
沒有提升,不過如果 let
真的沒有提升,應該會存取到 global scope 的 a = 1
才對。
特別的就在這, let
跟 const
其實也會被提升,只不過宣告的變數,不會被初始化為 undefined
,可以想像成 VO 裡有這個變數名稱的 key,但 value 是空的。因此在賦值之前試著存取該變數,都會出現錯誤。
前面有說過 let
跟 const
都是以大括號的 block 作為作用域,所以只要該 block 中有存取未宣告的變數,從進入 block 到賦值前,就會是 Temporal Dead Zone(TDZ),中文為暫時性死區,是一個為了解釋 let
與 const
的 hoisting 行為所提出的一個名詞。舉例來說:
1 | // function |
評論