六角學院 JS 核心筆記 (六)【執行環境與作用域】- 提升 (Hoisting)

前言

提升 (Hoisting) 在 JavaScript 中,對於變數和函式的宣告和運行順序是很重要的觀念。但是在 ECMAScript 中其實沒有這個專有名詞,「提升」只是一個大家的共識、共同的說法,它的相關概念是寫在 Execution Contexts 中。其實我剛學 JavaScript 時,覺得它真的很奇怪,為什麼不要像其他程式一樣,好好在最前面宣告完就沒事了呢?(摔

Hoisting 是什麼?

非函式宣告

如果直接執行下面的程式碼,會出現 Uncaught ReferenceError: a is not defined,因為沒有宣告變數 a,產生 RHS 錯誤

1
console.log(a);

但是,我如果把程式碼改成下面這樣,程式碼不會報錯,會印出 undefined,為什麼?

1
2
console.log(a); // 輸出結果:undefined
var a = 1;

JavaScript 的運行可以分為兩個階段:創造 / 編譯階段執行階段

上述的程式碼依照這兩個階段拆解,可以想像成這樣 (並不是真的移動程式碼):

1
2
3
4
5
6
// 創造 / 編譯階段
var a = undefined;

// 執行階段
console.log(a); // 輸出結果:undefined
a = 1;

在創造 / 編譯階段時,會先執行所有的宣告變數的動作。上述程式碼也就是會先產生 a 變數 並且賦予 undefined 這個初始值。因此當然輸出 undefined 結果。

宣告函式

函式表達式 (Function Expression)

下面這段程式碼,是函式表達式 (Function Expression),因為有 var 這個 identifier,Hoisting 的行為和非函式宣告是一樣的。它會產生 Uncaught TypeError: fn1 is not a function 錯誤。為什麼?

1
2
3
4
fn1();
var fn1 = function(){
console.log("Hello Jenifer");
};

可以拆解成:

1
2
3
4
5
6
7
8
// 創造 / 編譯階段
var fn1 = undefined;

// 執行階段
fn1();
fn1 = function(){
console.log("Hello Jenifer");
};

因為 fn1 一開始還是 undefined,當然不能當作函式來呼叫。而呼叫的時間是在執行階段,因此產生 RHS 其他錯誤

函式陳述式 (Function Statement)

下面這段程式碼,是函式陳述式 (Function Statement):

1
2
3
4
fn1();
function fn1(){
console.log("Hello Jenifer");
}

Hoisting 行為則非常不同,JS 在創造 / 編譯階段就會產生 fn1 變數 並賦予一個指向函式物件的 reference,而不是 undefined。可以拆解成:

1
2
3
4
5
6
7
// 創造 / 編譯階段
function fn1(){
console.log("Hello Jenifer");
}

// 執行階段
fn1(); // 輸出結果:Hello Jenifer

執行階段呼叫 fn1() 時,它已經有函式物件了,因此會順利輸出結果,不會報錯。

函式宣告方式參考資料:
JS 原力覺醒 Day07 - 陳述式 表達式

Hoisting 優先權

函式陳述式函式表達式非函式宣告有更高的優先權。

舉例一

1
2
3
4
5
6
7
8
9
10
11
12
// 函式表達式
var fn1 = function(){
console.log("Hello Jenifer");
};

// 函式陳述式
function fn1(){
console.log("寫在後面");
}

// 呼叫
fn1();

可以拆解成:

1
2
3
4
5
6
7
8
9
10
11
12
// 創造 / 編譯階段
function fn1(){
console.log("寫在後面");
}
var fn1; //因為前面有出現過 fn1 變數,因此不賦予 undefined

// 執行階段
fn1 = function(){
console.log("Hello Jenifer");
};

fn1(); // 輸出結果:Hello Jenifer

因為函式陳述式最優先,所以被提升到最前面,最後「被新的函式物件覆蓋」,反而是輸出 Hello Jenifer

舉例二

如果把上面的程式碼改成:

1
2
3
4
5
6
7
8
9
10
11
12
// 函式陳述式
function fn1(){
console.log("寫在後面");
}

// 呼叫
fn1();

// 函式表達式
var fn1 = function(){
console.log("Hello Jenifer");
};

可以拆解成:

1
2
3
4
5
6
7
8
9
10
11
// 創造 / 編譯階段
function fn1(){
console.log("寫在後面");
}
var fn1; //因為前面有出現過 fn1 變數,因此不賦予 undefined

// 執行階段
fn1(); // 輸出結果:寫在後面
fn1 = function(){
console.log("Hello Jenifer");
};

呼叫 fn1() 時,函式物件還沒有被新的內容覆蓋,因此輸出 寫在後面

如果想要了解基本的 Hoisting 觀念,到這裡就可以了。如果想理解背後運作的機制,可以前往:深入了解:Hoisting 和「執行環境、詞彙環境」的關係

參考資料:
六角學院:JavaScript 核心篇 - 邁向達人之路