iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 16
0
Modern Web

JavaScript Note系列 第 16

Hoisting 提升

在討論Hoisting之前,先來看下面例子。

var x = 10;
console.log(x);

結果,很簡單:10
那如果,執行下面的程式碼,結果會是怎樣呢?

console.log(x);
var x = 10;

也許你心中預期的答案會是這樣:
https://ithelp.ithome.com.tw/upload/images/20181031/20112573aGoNAu3tMs.png
但答案是:
https://ithelp.ithome.com.tw/upload/images/20181031/20112573nQ233K2NVN.png
在JavaScript中,undefind表示變數宣告後,尚未初始化。
出現undefind,也就表示這個變數在輸出之前就已經宣告了,但我們是在輸出之後,才宣告的,這是怎麼一回事?
JavaScript之所以會產生這樣的結果,是因為Hoisting這個觀念。

在MDN Web Docs中對於 Hoisting,是這樣描述的:

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

引用來源:https://developer.mozilla.org/zh-TW/docs/Glossary/Hoisting

要了解Hoisting之前,首先必須先了解全域執行環境這個概念。

全域執行環境的建立可以分為2個階段:1.創立階段 2.執行階段。

在創立階段,JavaScript引擎會掃描程式碼,對於變數與函式的宣告會預先處理,為其在記憶體中設置空間,這邊要注意的是,在這個階段JavaScript引擎只會對處理宣告,不會處理賦值(assignment)。

以var x = 10 來說,它只處理var x這部分,x=10的部分並沒有處理,執行階段才會把10指定給x。

在執行階段,來到console.log(x),因為x已設置在記憶體中,但無賦值,所以才會產生undefind。

那至於Hoisting這個用語是怎麼來的?回到剛剛的程式碼,其實可以用另一種角度來看。

var x;
console.log(x);
x = 10;

這樣就明白了,就好像把變數宣告移到範疇(scope)的最頂端,但它並不是真的移動程式碼,只是預先處理宣告的部分。

接下來是函式的部分。

x();
function x() {
	console.log('Hello World');
}

結果應該知道了吧:
https://ithelp.ithome.com.tw/upload/images/20181031/20112573Uu2cHg4cjY.png
沒錯,函式宣告也是會被提升的。在創立階段函式x跟變數宣告一樣,會被配置到記憶體中。

那下面的範例結果呢?

x();
var x = function () {
    console.log("Hello World");
}

結果:
https://ithelp.ithome.com.tw/upload/images/20181031/20112573x1I9tvR2ZV.png
原因你想到了嗎?剛剛我們提到JavaScript引擎只會處理宣告,不會處理賦值,不只是變數,即使是函式表達式(function expression)也一樣。

第一階段後,變數x是undefined,在執行階段呼叫x函式,還未執行到下面的函式表達式,JavaScript引擎根本就找不到x這個函式,所以會丟出錯誤訊息。

接下來,要帶入範疇(scope)的概念了,剛剛的變數都是在全域環境執行的,當有Hoisting的情形發生,宣告自然是提升到全域環境最頂端,那如果是區域變數呢?

getName();
function getName() {
    console.log(myName);
    var myName = 'Bill';
}

結果:
https://ithelp.ithome.com.tw/upload/images/20181031/20112573Q5lvpRSdfZ.png
首先要注意的是,Hoisting是在宣告的範疇內進行的,上面的myName是宣告在getName( )的區域變數,所以會提升至該函式內的最頂端,用另一種角度來看:

getName();
function getName() {
    var myName;
    console.log(myName);
    myName = 'Bill';
}

再來看這個範例。

var x = 'global';
scope();
function scope() {
    console.log(x);
    var x = 'local';
    console.log(x);
}

可能預期的結果:
https://ithelp.ithome.com.tw/upload/images/20181031/20112573zeLvEKObh9.png
但正確的結果:
https://ithelp.ithome.com.tw/upload/images/20181031/201125730NTSAudJBe.png
會發生這樣的結果,是因為範疇的規則:
在函式中定義和全域變數同名的區域變數,如此一來,整個函式都看不到同名的全域變數。
因此函式內的程式碼另一種角度來看,變數宣告會提升至範疇的最頂端:

function scope() {
    var x;
    console.log(x);
    x = 'local';
    console.log(x);
}

那如果是全域變數與全域函式撞名呢?

var x;
function x() {
    console.log('Hello World');
}
console.log(x);

結果:
https://ithelp.ithome.com.tw/upload/images/20181031/20112573TAcZjjF4BE.png
顯然是函式的優先權大於變數。

如果對x賦值的話,便會蓋過函式拿到優先權:

var x = 'global x';
function x() {
    console.log('function x');
}
console.log(x);

https://ithelp.ithome.com.tw/upload/images/20181031/20112573onzK0e7tUN.png
如果呼叫函式,會產生錯誤:

var x = 'global x';
function x() {
    console.log('function x');
}
x();

https://ithelp.ithome.com.tw/upload/images/20181031/20112573m01WYJuobw.png

即便你對程式的流程瞭如指掌,如果對於Hoisting,這個在JS很特殊的行為不熟悉的話,還是有可能發生意想不到的錯誤,了解此特性,更應該要養成良好的習慣。
讓變數宣告盡可能地靠近被使用的地方,或是宣告在範疇的最頂端。

參考來源:
JavaScript全攻略:克服JS的奇怪部分 執行環境:創造與提升


上一篇
Scope 作用域
下一篇
This
系列文
JavaScript Note31

1 則留言

0
hbdoy
iT邦新手 5 級 ‧ 2018-10-31 19:01:41

您不澄清前幾篇疑似抄襲的疑點,這樣讓誤會一直持續下去好嗎?

WM iT邦新手 5 級‧ 2018-10-31 20:26:28 檢舉

已在其他地方回應

我要留言

立即登入留言