JavaScript 核心觀念(6)-執行環境與作用域-提升

前言

提升(Hoisting) 可以說是我最喜好的一個觀念,JavaScript 中舉凡函式、變數都有這個觀念存在。

提升

提升(Hoisting)

其實我會覺得喜歡這個觀念是在於變數還沒有被宣告但可以提前使用,例如下方這一段程式碼

1
2
console.log(a); // undefined
var a = 'Ray';

在這邊我們可以看到 console.log(a) 是在「宣告變數之前」,先不說 JavaScript 沒有出現任何錯誤這件事情,但它卻出現一個神奇的值,也就是 undefined

而這個狀況稱之為 提升、拉升、Hoisting 等等,此外這邊如果你去查 MDN 看 Hoisting 的說明更會嚇到吃手手,為什麼呢?制定 JavaScript 的 ECMAScript (Ecma國際) 並沒有這個專有名詞,因此 Hoisting 這個說明僅僅只是要釐清 JavaScript 的運作原理而出現的觀念說明。

以下截至 MDN 說明

提升(Hoisting)是在 ECMAScript® 2015 Language Specification 裡面找不到的專有名詞。它是一種釐清 JaveScript 在執行階段內文如何運行的思路(尤其是在創建和執行階段)。然而,提升一詞可能會引起誤解:例如,提升看起來是單純地將變數和函式宣告,移動到程式的區塊頂端,然而並非如此。變數和函數的宣告會在編譯階段就被放入記憶體,但實際位置和程式碼中完全一樣。

因此 JavaScript 可以區分為兩個階段來釐清運作原理,分別為 創造階段 以及 執行階段,這邊舉例上方程式碼來拆解的話就會變成這樣

創造階段

1
2
var a;
console.log(a);

執行階段

1
2
console.log(a); // undefined
a = 'Ray';

使用 Hoisting 的觀念去了解 JavaScript 運作原理是一個不錯的方式,但是這邊還是要強調一次 「Hoisting 一整個過程都是與記憶體有關係,但實際 JavaScript 並沒有這個專有名詞存在」。

首先當變數或是函式被宣告時,就會被 JavaScript 優先準備一個洞讓相關宣告可以放進記憶體中,而這樣的設計最好的優點在於程式碼可以在該宣告之前被呼叫以及使用,因此 console.log(a); 才會出現 undefined 並且不會出現錯誤而導致系統中斷。

那至於為什麼變數會出現 undefined 呢?這個的原因在於 JavaScript 預先準備好記憶體時,必須先準備一個初始值所導致,因此若假使 JavaScript 沒有給予變數一個初始值,那麼就有可能出現錯誤,所以才會有 undefined 的存在。

函式比變數有更高的優先權

在前面所講的的範例是變數,那在這邊函式在宣告時,其實會比變數有更高的優先權,函式會比變數更優先被建立並放進記憶體中,但這邊僅限於函式陳述式,在後面章節會在介紹什麼是函式陳述式以及函式表達式,目前你只需要知道我們一般常見的函式宣告都是屬於函式陳述式,例如以下程式碼

1
2
3
function sayHi() {
...
}

因此注意函式表達式,其實是不吃預先放進記憶體這一套,在後面章節也會介紹什麼是函式表達式。

1
2
3
var a = sayHi() {
...
};

讓我們回歸主題,為什麼函式比變數有更高的優先權呢?舉例以下程式碼來講,我們前面已經知道變數宣告時會被放進記憶體,此時變數會被賦予一個預設初始值,因此若在變數宣告之前呼叫該變數就會出現 undefined,那函式呢?舉例以下這一個範例程式碼

1
2
3
4
5
var a = 'Ray';
function a() {
console.log('Hello');
}
console.log(a); // Ray

我們可以看到,明明函式是在變數之後被宣告,但出現的卻是 Ray,那這是一個相當神奇的狀況,正常來講你或許會認為這邊應該要出現的是 function a() { ... },但這邊前面有說過我們可以使用 Hoisting 的觀念來釐清其 JavaScript 運作原理,首先我們先區分出創造階段以及執行階段

創造階段
在前面有說過因為函式比變數有更高的優先權,所以會比變數更優先被建立

1
2
3
4
function a() {
console.log('Hello');
}
var a;

執行階段

1
2
a = 'Ray';
console.log(a); // Ray

透過上方範例可以了解到函式是比變數有更高的優先權被優先建立,但若函式沒有較高的優先權的話,那麼出來的就會不一樣。

此外因為函式會被優先建立的關係,因此你可以在函式之前呼叫該函式

1
2
3
4
sayHi(); // Hello Ray
function sayHi() {
console.log('Hello Ray');
}

Hoisting 小測驗

接下來課程上也有提供一個小練習,可以透過該小練習了解其運作原理

練習題

1
2
3
4
5
6
7
8
whosName()
function whosName() {
if (myName) {
myName = '杰倫';
}
}
var myName = '小明';
console.log(myName);

以下為解答

創造階段
因為函式比變數有更高的優先權,所以會優先被建立

1
2
3
4
5
6
function whosName() {
if (myName) {
myName = '杰倫';
}
}
var myName;

注意這時候建立好的函式就已經準備好等待被呼叫並執行,但並不代表創造階段 whosName() 就已經被執行,換言之真正的執行行為,僅會在執行階段時運作。

執行階段

1
2
3
whosName();
myName = '小明';
console.log(a); // 小明

由於 whosName 執行時 myName 還沒有被賦予值,因此 myName = undefined,當遇到 if 判斷式時則會被型別轉換成 false(undefined = false) 因此並不會進入 myName = '杰倫';

所以其實 Hoisting 一整個運作的原理都主要與記憶體有關,也因此這就是為什麼 ECMAScript 中找不到 Hoisting 這個專有名詞,因為 Hoisting 這個東西只是為了方便我們理解 JavaScript 運作而出現的東西而已。

參考文獻

0%