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

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

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

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

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

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

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

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

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

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

Copy link
On this page
JavaScript 裡的文章置頂效果 —— Hoisting
Hoisting 置頂了宣告的效果
變數宣告的 Hoisting 效果 (透過 var 關鍵字)
變數的 Hoisting 效果只有「宣告」的部分,不包含「初始化」(Initializations)
函數也有 Hoisting 效果 (透過 function 關鍵字)
透過變數方式宣告的函數,Hoisting 效果比照變數
使用 let 或 const 宣告的變數不具備 Hoisting 效果
補充:真相是 let 和 const 其實也有 Hoisting
總結 Hoisting
References