[JavaScript] Hoisting 為什麼在宣告前就可以使用?
Cathy P

前言

如果在學習 JavaScript 之前學習過其他程式語言,應該都會發現 JavaScript 跟其他程式語言的其中一個不同之處:變數和 function 可以在宣告之前使用

JavaScript 的 Hoisting 讓我可以在變數和 function 宣告前就使用它們,此篇文章將介紹 Hoisting 是什麼?以及它是如何運作的?如果你也曾對此感到好奇或困惑,請繼續閱讀!

Hoisting 是什麼?

在 JavaScript 中,如果我們試著對一個還沒有宣告過的變數取值,會得到以下的錯誤:

1
console.log(variable); // Uncaught ReferenceError: variable is not defined

但如果我們改成:

1
2
console.log(variable); // undefined
var variable;

發現程式竟然不再報錯了,竟然順利執行且回傳了undefined

這種在宣告之前就可以使用變數的情況就叫做「Hoisting 提升」,Hoisting 會把宣告的部分挪到 Scope 的最上方,像是這樣:

1
2
var variable;
console.log(variable); // undefined

雖然 Hoisting 看起來挪動了宣告變數的程式碼,實質上程式碼的位置並不會移動,而是是在編譯階段經過處理的達成這種效果的。為了方便講解,我仍然繼續用「宣告變數的程式碼被挪到上方」來稱呼這個行為。


要注意的是,僅有「宣告變數」的部分會提升,但 Initialization 初始化(也就是賦值的部分)並不會提升。
所以下面的程式印出的仍然是 undefined 而不是 100

1
2
console.log(variable); // undefined
var variable = 100;

因為 Hoisting 將宣告的程式碼挪到上方,而初始化的部分不動,上面的程式碼可以看作成:

1
2
3
var variable;
console.log(variable); // undefined
variable = 100;

只有宣告變數的地方被提升了,賦值的程式碼仍然在 console.log(varaible); 的下一行才執行,所以印出的自然是 undefined

除了變數宣告會被提升之外,function 宣告也同樣會被提升,所以我們可以在 function 被宣告之前就呼叫 function。

1
2
3
4
hello(); // "Hello World!"
function hello() {
console.log("Hello World!");
}

但是只有 function declaration(函式宣告)會被提升,function expression(函式表達式)是不會被提升的。

所以剛剛的例子如果寫成 function expression,則會引發錯誤:

1
2
3
4
hello(); // Uncaught TypeError: hello is not a function
var hello = function () {
console.log("Hello World!");
};

經過 Hoisting 程式碼看起來像是:

1
2
3
4
5
var hello;
hello(); // Uncaught TypeError: hello is not a function
hello = function () {
console.log("Hello World!");
};

因為只有宣告會被提升,在呼叫hello()的時候,hello還不是一個 function,因此發生了錯誤。

Function 內的 Hoisting

了解了 Hoisting 的基本定義與原理之後,我們再接著看 function 內的 hoisting 的運作。

之前宣告的變數都是在 function 外,宣告會被提升到 global scope 的最上面。
function 內宣告的變數則會提升到 function scope 的最上面

1
2
3
4
5
function callName() {
console.log(name);
var name = "Yuju";
}
callName(); // undefined

將宣告變數的地方提升到 function scope 的最上面,經過 hoisting 我們可以看成是:

1
2
3
4
5
6
function callName() {
var name;
console.log(name);
name = "Yuju";
}
callName(); // undefined

接著看下一個例子:

1
2
3
4
function callName(name) {
console.log(name);
}
callName("Yerin"); // "Yerin"

這個程式碼雖然沒有很明確的用一行 var name; 來宣告變數,但參數本身也可以看成是一個宣告變數的過程,加上把參數傳進去的過程,我們可以想像成:

1
2
3
4
5
6
function callName(name) {
var name;
name = "Yerin";
console.log(name);
}
callName("Yerin"); // "Yerin"

再來我們看一個變化題:

1
2
3
4
5
function callName(name) {
console.log(name);
var name = "Sowon";
}
callName("Yerin"); // "Yerin"

按照之前說的,將宣告變數的程式碼挪到上面,name = "Sowon"; 仍留在原地,如下:

1
2
3
4
5
6
7
8
function callName(name) {
var name;
var name;
name = "Yerin";
console.log(name);
name = "Sowon";
}
callName("Yerin"); // "Yerin"

雖然 name = "Sowon";console.log(name); 之後,但參數傳進來時本身就賦值了,所以印出的會是"Yerin"而不是undefined,也不是 "Sowon:

Function 宣告與變數宣告的優先權

如果有 function 和變數同名,function 的優先權比較高,如果有多個 function 重名,後面的 function 會取代前面的 function。

1
2
3
console.log(hello); // fn hello(){}
function hello() {}
var hello;
1
2
3
4
5
6
console.log(hello); // ƒ hello() {console.log("hello")}
function hello() {}
function hello() {
console.log("hello");
}
var hello;

let 和 const 的 Hoisting

在寫這篇文章之前,我一直以為使用 let 和 const 宣告的變數,並不會被提升。
我們在執行以下程式碼的時候,也會發現變數看起來並沒有被提升而引發了報錯:

1
2
console.log(a); // Uncaught ReferenceError: variable is not defined
let a;

但事實上並非如此,使用 let 和 const 宣告的變數一樣會被提升到 scope 的最上面,只是和用 var 宣告的變數不一樣,使用 let 和 const 宣告的變數並未被初始化(Initialization),所以在賦值之前並不能使用。

我們可以透過下面的程式碼來佐證:

1
2
3
4
5
6
var name = "SinB";
function hello() {
console.log(name);
let name;
}
hello();

我們推演一下 let 不會被提升的情況, let name; 宣告在 console.log(name) 之後且我們假設不會被提升,所以照理來說第三行的 console.log(name); 應該會存取到第一行的 var name = "SinB;" 而印出 "SinB"

但實際上系統會報出 Uncaught ReferenceError: Cannot access 'name' before initialization的錯誤訊息。

所以實際上,let 和 const 宣告的變數也是會被提升的,只是在賦值之前我們不能使用它,才會有看起來不會被提升的錯覺。

Temporal dead zone(暫時死區)

在 let 和 const 宣告的變數之後、賦值之前,變數不可取用的那個期間,我們稱作 Temporal dead zone,簡稱 TDZ。
如果在 TDZ 的期間存取變數,則會發生 ReferenceError 的錯誤。

1
2
3
4
5
6
7
8
9
10
function hello() {
// 變數 gender 的 TDZ 開始
let name = "Umji";

console.log(name);
console.log(gender); // 仍在 TDZ 期間所以發生 ReferenceError 錯誤

let gender = "female"; // 變數 gender 的 TDZ 結束
}
hello(); // Uncaught ReferenceError: Cannot access 'gender' before initialization

function 中的預設參數也會有 TDZ:

1
2
function hello(a = b, b = "Eunha") {}
hello(); // Uncaught ReferenceError: Cannot access 'b' before initialization

因為在變數 b 的 TDZ 期間嘗試取值所以發生了錯誤。

Typeof

在 JavaScript 中,我們可以使用 Typeof 來檢查變數是否被宣告。

所以在下面程式碼的情況下,雖然我們沒有宣告過 a,但只會回傳 undefined 而不會發生錯誤。

1
console.log(typeof a); // undefined

第二個例子,變數宣告會被提升到上方,但賦值的程式碼仍維持在下方,程式一樣是回傳 undefined:

1
2
console.log(typeof a); // undefined
var a = "apple";

如果改成使用 let 宣告,則會因為在 TDZ 的期間引發錯誤:

1
2
3
// 變數 a 的 TDZ 開始
console.log(typeof a); // 仍在 TDZ 期間所以發生 ReferenceError 錯誤
let a = "apple"; // 變數 a 的 TDZ 結束

Hoisting 的用途

  1. 在宣告之前取用變數
    但這其實不是一個好的習慣,雖然有 Hoisting 但我們仍要避免在宣告變數前取用變數。
    甚至我們應該盡量使用 let 與 const,而不是用 var 來宣告變數。
  2. 在 function 宣告之前呼叫 function
    這點來說幫助就非常大了,很多時候我們習慣把 function 宣告放在下方。
  3. 讓 function 可以互相呼叫
    如果 function 之間沒辦法呼叫,function 互相呼叫程式就沒辦法執行:
1
2
3
4
5
6
7
8
9
10
11
function add(a, b) {
check(++a, ++b);
}
function check(a, b) {
if (a + b < 10) {
add(a, b);
} else {
console.log(a, b);
}
}
add(0, 0);

總結

先簡單做個重點整理:

  1. var 宣告會被提升。
  2. Initialization 的部分不會被提升。
  3. let 和 const 雖然會被提升,但賦值之前無法取值。
  4. function declaration 會被提升,而 function expression 不會。
  5. function 與變數同名,function 優先。

在寫這篇文章之前,我對 Hoisting 的認識只有「用 var 宣告會提升,而用 let 和 const 不會提升。」這樣粗淺的認識。

仔細研究之後,才發現 Hoisting 的運作其實大有文章,有非常多的細節在其中,比我原本想像中的複雜許多。

經過一番瞭解與消化後,才整理出這篇文章,希望對正在讀這篇文章的你有幫助。第一次打一篇比較正式的 js 文章,如果有任何錯誤,也煩請指正,謝謝。


參考資料:
  1. MDN: Hoisting
  2. Understanding Hoisting in JavaScript
  3. What is Hoisting in JavaScript?
  4. 我知道你懂 hoisting,可是你了解到多深?