前言
如果在學習 JavaScript 之前學習過其他程式語言,應該都會發現 JavaScript 跟其他程式語言的其中一個不同之處:變數和 function 可以在宣告之前使用。
JavaScript 的 Hoisting 讓我可以在變數和 function 宣告前就使用它們,此篇文章將介紹 Hoisting 是什麼?以及它是如何運作的?如果你也曾對此感到好奇或困惑,請繼續閱讀!
Hoisting 是什麼?
在 JavaScript 中,如果我們試著對一個還沒有宣告過的變數取值,會得到以下的錯誤:
1 | console.log(variable); // Uncaught ReferenceError: variable is not defined |
但如果我們改成:
1 | console.log(variable); // undefined |
發現程式竟然不再報錯了,竟然順利執行且回傳了undefined
!
這種在宣告之前就可以使用變數的情況就叫做「Hoisting 提升」,Hoisting 會把宣告的部分挪到 Scope 的最上方,像是這樣:
1 | var variable; |
雖然 Hoisting 看起來挪動了宣告變數的程式碼,實質上程式碼的位置並不會移動,而是是在編譯階段經過處理的達成這種效果的。為了方便講解,我仍然繼續用「宣告變數的程式碼被挪到上方」來稱呼這個行為。
要注意的是,僅有「宣告變數」的部分會提升,但 Initialization 初始化(也就是賦值的部分)並不會提升。
所以下面的程式印出的仍然是 undefined
而不是 100
。
1 | console.log(variable); // undefined |
因為 Hoisting 將宣告的程式碼挪到上方,而初始化的部分不動,上面的程式碼可以看作成:
1 | var variable; |
只有宣告變數的地方被提升了,賦值的程式碼仍然在 console.log(varaible);
的下一行才執行,所以印出的自然是 undefined
。
除了變數宣告會被提升之外,function 宣告也同樣會被提升,所以我們可以在 function 被宣告之前就呼叫 function。
1 | hello(); // "Hello World!" |
但是只有 function declaration(函式宣告)會被提升,function expression(函式表達式)是不會被提升的。
所以剛剛的例子如果寫成 function expression,則會引發錯誤:
1 | hello(); // Uncaught TypeError: hello is not a function |
經過 Hoisting 程式碼看起來像是:
1 | var hello; |
因為只有宣告會被提升,在呼叫hello()
的時候,hello
還不是一個 function,因此發生了錯誤。
Function 內的 Hoisting
了解了 Hoisting 的基本定義與原理之後,我們再接著看 function 內的 hoisting 的運作。
之前宣告的變數都是在 function 外,宣告會被提升到 global scope 的最上面。
而 function 內宣告的變數則會提升到 function scope 的最上面 。
1 | function callName() { |
將宣告變數的地方提升到 function scope 的最上面,經過 hoisting 我們可以看成是:
1 | function callName() { |
接著看下一個例子:
1 | function callName(name) { |
這個程式碼雖然沒有很明確的用一行 var name;
來宣告變數,但參數本身也可以看成是一個宣告變數的過程,加上把參數傳進去的過程,我們可以想像成:
1 | function callName(name) { |
再來我們看一個變化題:
1 | function callName(name) { |
按照之前說的,將宣告變數的程式碼挪到上面,name = "Sowon";
仍留在原地,如下:
1 | function callName(name) { |
雖然 name = "Sowon";
在 console.log(name);
之後,但參數傳進來時本身就賦值了,所以印出的會是"Yerin"
而不是undefined
,也不是 "Sowon:
。
Function 宣告與變數宣告的優先權
如果有 function 和變數同名,function 的優先權比較高,如果有多個 function 重名,後面的 function 會取代前面的 function。
1 | console.log(hello); // fn hello(){} |
1 | console.log(hello); // ƒ hello() {console.log("hello")} |
let 和 const 的 Hoisting
在寫這篇文章之前,我一直以為使用 let 和 const 宣告的變數,並不會被提升。
我們在執行以下程式碼的時候,也會發現變數看起來並沒有被提升而引發了報錯:
1 | console.log(a); // Uncaught ReferenceError: variable is not defined |
但事實上並非如此,使用 let 和 const 宣告的變數一樣會被提升到 scope 的最上面,只是和用 var 宣告的變數不一樣,使用 let 和 const 宣告的變數並未被初始化(Initialization),所以在賦值之前並不能使用。
我們可以透過下面的程式碼來佐證:
1 | var name = "SinB"; |
我們推演一下 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 | function hello() { |
function 中的預設參數也會有 TDZ:
1 | function hello(a = b, b = "Eunha") {} |
因為在變數 b 的 TDZ 期間嘗試取值所以發生了錯誤。
Typeof
在 JavaScript 中,我們可以使用 Typeof 來檢查變數是否被宣告。
所以在下面程式碼的情況下,雖然我們沒有宣告過 a,但只會回傳 undefined 而不會發生錯誤。
1 | console.log(typeof a); // undefined |
第二個例子,變數宣告會被提升到上方,但賦值的程式碼仍維持在下方,程式一樣是回傳 undefined:
1 | console.log(typeof a); // undefined |
如果改成使用 let 宣告,則會因為在 TDZ 的期間引發錯誤:
1 | // 變數 a 的 TDZ 開始 |
Hoisting 的用途
- 在宣告之前取用變數
但這其實不是一個好的習慣,雖然有 Hoisting 但我們仍要避免在宣告變數前取用變數。
甚至我們應該盡量使用 let 與 const,而不是用 var 來宣告變數。 - 在 function 宣告之前呼叫 function
這點來說幫助就非常大了,很多時候我們習慣把 function 宣告放在下方。 - 讓 function 可以互相呼叫
如果 function 之間沒辦法呼叫,function 互相呼叫程式就沒辦法執行:
1 | function add(a, b) { |
總結
先簡單做個重點整理:
- var 宣告會被提升。
- Initialization 的部分不會被提升。
- let 和 const 雖然會被提升,但賦值之前無法取值。
- function declaration 會被提升,而 function expression 不會。
- function 與變數同名,function 優先。
在寫這篇文章之前,我對 Hoisting 的認識只有「用 var 宣告會提升,而用 let 和 const 不會提升。」這樣粗淺的認識。
仔細研究之後,才發現 Hoisting 的運作其實大有文章,有非常多的細節在其中,比我原本想像中的複雜許多。
經過一番瞭解與消化後,才整理出這篇文章,希望對正在讀這篇文章的你有幫助。第一次打一篇比較正式的 js 文章,如果有任何錯誤,也煩請指正,謝謝。
參考資料: