基本的Hoisting行為

Hoisting 的行為可以分為兩種:

  1. 變數提升
  2. 函式提升
  • 變數提升:
1
2
3
4
console.log(x);
var x = 1;

//印出結果:undefined

從上面印出結果可以發現,還未宣告的x竟然印出結果是undefined,而不是 not defined就是Hoisting的作用之一

  • 函式提升:
1
2
3
4
5
6
7
8
logx();

function logx(){
var x = 1;
console.log(x)
};

// 印出結果:1

在宣告函式之前就呼叫函式卻可以正常輸出結果也是Hoisting的效果

什麼是Hoisting

當你完全沒有宣告變數就印出時

1
2
console.log(v)

會得到變數沒有定義的錯誤

雖然程式是一行一行的讀取但是在JS裡面卻可以這樣寫

1
2
console.log(v);
var v

得出結果是undefined,竟然沒有報錯,依照理解這邊應該也會報錯才對,這樣的現象就是Hositing

第二行的 變數v 因為某些原因被提升到了最開頭,因此你可以把目前的程式碼想像成這樣:

1
2
var v
console.log(v);

這部分必須由JS引擎的行為來解釋,它並不是真的移動變數v到頂端!

hoisting 只會提升宣告而非賦值

可以把 var v = 5理解成

  1. 宣告 v
  2. v = 5

而Hoisting只會提升第一階段也就是宣告的部分

範例解釋 一

1
2
3
console.log(v);
var v = 5;
// 印出結果 是 undfined並不是5

提升後可以想像成這樣:

1
2
3
4
var v;
console.log(v);
v = 5;
// 印出結果 是 undfined並不是5

範例解釋 二

這個輸出的內容會是什麼?

1
2
3
4
5
function test(v){
console.log(v)
var v = 3
}
test(10)

因為傳進來的參數讓 v = 10所以可以這樣理解:

  1. var v = 10
  2. var v 是因為下面的var v = 3的變數提升上來
  3. 這邊雖然因為變數提升上來了var v但是因為沒有重新賦值給v所以還是保持10這個結果
1
2
3
4
5
6
7
8
9
10

function test(v){
var v = 10
var v
console.log(v)
var v = 3
}
test(10)

// 印出結果為 10

Hoisting函式優先級高於變數

這邊印出的結果是[Function: a],誠如最上面所解釋的Hoisting也支援函式宣告,並且可以從下方範例理解,函式的優先級高於變數,因此輸出的結果是函式本身而不是undfined

1
2
3
4
5
console.log(a) 
var a
function a(){}

// 印出結果: [Function: a]

Hoisting重點

  1. 變數以及函式都會有提升發生
  2. 函式提升的優先級高於變數
  3. 要注意參數在函式內部的參與會給變數賦值
  4. Hoisting只會提升宣告,不會提升賦值

let 跟 const 與 hoisting

let 與 const 也有 hoisting 但沒有初始化為 undefined,而且在賦值之前試圖取值會發生錯誤(TDZ)。

1
2
3
console.log(a) 
let a
// ReferenceError: Cannot access 'a' before initialization

In ECMAScript 2015, let bindings are not subject to Variable Hoisting, which means that let declarations do not move to the top of the current execution context. Referencing the variable in the block before the initialization results in a ReferenceError (contrary to a variable declared with var, which will just have the undefined value). The variable is in a “temporal dead zone” from the start of the block until the initialization is processed.

MDN

為什麼我們需要 hoisting?

  • 為了解決function 的相互呼叫

範例

  1. 呼叫函式帶入參數10 loop(10)
  2. 進入函式loop內,10>1 故 logEvenOrOdd(n=10-1)帶入9
  3. 呼叫logEvenOrOdd(9) - 這邊就是相互呼叫如果沒有變數提升是做不到的!
  4. 印出 9, 9除以2的餘數為1故為true 顯示’Odd’
  5. 重新呼叫一次loop(n)
  6. 反覆行為
  7. .
  8. .
  9. .
1
2
3
4
5
6
7
8
9
10
11
12
function loop(n) {
if (n > 1) {
logEvenOrOdd(--n)
}
}

function logEvenOrOdd(n) {
console.log(n, n % 2 ? 'Odd' : 'Even')
loop(n)
}

loop(10)

印出結果:

Hoisting 到底是怎麼運作的?

先簡單介紹一下執行背景 (Execution Contexts)

  • 是一段別人寫好的程式來驗證以及執行使用者的程式碼
  • 管理程式碼執行的先後順序(也就是stack執行堆)
  • 管理的內容不只是使用者撰寫的還有更多

Hoisting運作

簡單來說執行函式的時候就會創造執行背景,並且會依序做三件事前在函式執行之前:

Variable Object存在於EC環境內在裡面宣告的變數跟函式都會被加進 VO 裡面

  1. 把參數放到 Variable Object 裡面並設定好值,傳什麼進來就是什麼,沒有值的設成 undefined(這部分也就是變數的預設值為undefined的原因)
  2. 把 function 宣告放到 Variable Object 裡,如果已經有同名的就覆蓋掉(這邊解釋function優先級高於變數的原因)
  3. 把變數宣告放到 Variable Object 裡,如果已經有同名的則忽略(變數提升只會提升宣告而不提升賦值的原因)

再理解一次Hoisting

1
2
3
4
5
function test(v){
console.log(v)
var v = 3
}
test(10)
  1. 第一個階段是進入 EC
  2. 建立 VO
    1
    2
    3
    {
    v: 10
    }
  3. 因為VO內部已經有V這個屬性了因此忽略 v = 3,所以不添增VO的內容
  4. 第二階段執行程式碼 印出結果: 10

參考文章:

我知道你懂 hoisting,可是你了解到多深? by Huli