Day 10:程式也懂電梯向上? —— Hoisting
有沒有注意過 JavaScript 裡一個神奇的現象?
比如以下程式:
1
console.log(x);
Copied!
執行結果:
1
Uncaught ReferenceError: x is not defined
Copied!
(Source: 白爛貓貼圖)
因為使用了根本不存在的變數 x,執行時拋出錯誤,非常合理。
為了彌補,我們趕快增加 x 的變數宣告:
1
console.log(x);
2
var x = "OneJar";
Copied!
你程式老師在你後面,他非常火。
(Source: Youtube)
變數宣告當然是要在使用之前,補在後面有什麼用,一樣會先執行到拋錯的那一行。
執行結果:
1
undefined
Copied!
(Source: 白爛貓貼圖)
Why?為什麼宣告在使用之後不會拋錯?而且印了個莫名的答案。
我知道了。
(Source: 網路)
喂,冷靜。
程式不會變魔術,一定有理可循。
其實背後的原因,就是今天文章要介紹的 Hoisting

JavaScript 裡的文章置頂效果 —— Hoisting

Hoisting 這個術語在 MDN 裡翻譯為「提升」,但我覺得這個名詞太抽象,概念上不易理解。
我個人偏好翻成「宣告置頂」。
就像生活中網路論壇常見的置頂文章,當一些文章被設定為置頂文,無論你進入討論版的下一步想做什麼,這些置頂文都會優先被看到。
Hoisting 的效果非常類似這樣的概念。

Hoisting 置頂了宣告的效果

以下是 W3Schools 裡的介紹:
Hoisting is JavaScript's default behavior of moving all declarations to the top of the current scope.
Hoisting 是 JavaScript 的預設行為,把所有宣告效果提到當前 Scope 的頂端
也就是說,在正式執行程式之前,JavaScript 會先偷跑一個動作——把程式碼中宣告的部分提前到所屬 Scope 的頂端。

變數宣告的 Hoisting 效果 (透過 var 關鍵字)

例如文章開頭舉的例子:
1
console.log(x);
2
var x = "OneJar";
Copied!
運作上等同於:
1
var x;
2
console.log(x);
3
x = "OneJar";
Copied!
這就是為什麼 JavaScript 變數可以在宣告之前就使用,而不會拋錯。
但我的程式碼明明是 var x = "OneJar";,宣告同時就給予初始值,為何印出來的結果是 undefined 而非 "OneJar"

變數的 Hoisting 效果只有「宣告」的部分,不包含「初始化」(Initializations)

再引用 W3Schools 裡的原文:
JavaScript Initializations are Not Hoisted.
也就是說,只有宣告的部分會被提升。
這個觀念不那麼直觀,因為不是以一整行的程式碼來看,而是單獨抽出了程式碼中「宣告」的部分。
var x = "OneJar"; 這行程式來說,裡面包含 2 個動作: 1. var x:宣告一個變數名叫 x。 2. x = "OneJar":將賦值給變數 x (撰寫時將宣告和賦值寫在同一行,這個動作也稱為「初始化」)。
Hoisting 的效果只涵蓋動作 1,不涵蓋動作 2。
所以這段程式:
1
console.log(x);
2
var x = "OneJar";
Copied!
經過 Hoisting 後等同於:
1
var x;
2
console.log(x);
3
x = "OneJar";
Copied!
而非:
1
var x = "OneJar";
2
console.log(x);
Copied!

函數也有 Hoisting 效果 (透過 function 關鍵字)

以下這種寫法一定不陌生:
1
sayHi();
2
3
function sayHi(){
4
console.log('Hi');
5
}
Copied!
執行結果:
1
Hi
Copied!
是否曾經覺得奇怪,為什麼函數 sayHi() 宣告定義在後面,卻可以提前呼叫?
這也是 Hoisting 效果。
使用 function 關鍵字去宣告函數,整個函數定義都會被提到 Scope 最前面。
1
function sayHi(){
2
console.log('Hi');
3
}
4
5
sayHi();
Copied!

透過變數方式宣告的函數,Hoisting 效果比照變數

定義函數除了直接使用 function 做宣告和定義,也允許用「var 變數宣告 + function 函數定義」的寫法。
但需要注意的是,就像前面提到:Hoisting 效果只有「宣告」的部分,不包含「初始化」。
例如以下例子:
1
console.log( sayHi );
2
console.log( sayHi() );
3
4
var sayHi = function(){
5
return "Hi";
6
};
Copied!
執行結果:
1
undefined
2
Uncaught TypeError: sayHi is not a function
Copied!
被提升的只有「var 變數宣告」的部分,「function 函數定義」的部分仍在原本的位置。
運作上的效果就像以下程式碼:
1
var sayHi;
2
3
console.log( sayHi );
4
console.log( sayHi() );
5
6
sayHi = function(){
7
return "Hi";
8
};
Copied!

使用 letconst 宣告的變數不具備 Hoisting 效果

W3Schools:
Variables and constants declared with let or const are not hoisted!
Day8 文章介紹到 ES6 導入新的變數宣告關鍵字:letconst
需要注意到,這兩個關鍵字所宣告的變數不會有 Hoisting 效果。
1
console.log(x);
2
let x = "OneJar";
Copied!
執行結果:
1
Uncaught ReferenceError: x is not defined
Copied!

補充:真相是 letconst 其實也有 Hoisting

感謝邦友 Caesar 提供一篇文章——我知道你懂 hoisting,可是你了解到多深?,才了解其實 letconst 有 hoisting,只是行為不一樣。
例如以下範例:
1
let x = "OneJar";
2
3
function test(){
4
console.log(x);
5
let x;
6
}
7
test();
Copied!
如果沒有 hoisting,理論上應該會根據 Scope Chain 找到外面的 x,印出 "OneJar"
但實際上的執行結果:
1
Uncaught ReferenceError: x is not defined
Copied!
原因簡單來說,節錄文章的一句話:
let 與 const 也有 hoisting 但沒有初始化為 undefined,而且在賦值之前試圖取值會發生錯誤。
文章作者花了很多篇幅講解 hoisting 背後的運作原理,方知小小的 hoisting 觀念要深入,細節也是無窮無盡。

總結 Hoisting

Hoisting 效果包含: 1. 使用 var 的變數宣告。 2. 使用 function 宣告的函數與其定義。
Hoisting 效果不包含: 1. 初始化的部分 (Initializations),例如變數初始值或使用 var 宣告的函數定義。 2. 使用 letconst 的變數宣告。 (實際上有,但行為和 var 不一樣)

References