在討論Hoisting之前,先來看下面例子。
var x = 10;
console.log(x);
結果,很簡單:10
那如果,執行下面的程式碼,結果會是怎樣呢?
console.log(x);
var x = 10;
也許你心中預期的答案會是這樣:
但答案是:
在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');
}
結果應該知道了吧:
沒錯,函式宣告也是會被提升的。在創立階段函式x跟變數宣告一樣,會被配置到記憶體中。
那下面的範例結果呢?
x();
var x = function () {
console.log("Hello World");
}
結果:
原因你想到了嗎?剛剛我們提到JavaScript引擎只會處理宣告,不會處理賦值,不只是變數,即使是函式表達式(function expression)也一樣。
第一階段後,變數x是undefined,在執行階段呼叫x函式,還未執行到下面的函式表達式,JavaScript引擎根本就找不到x這個函式,所以會丟出錯誤訊息。
接下來,要帶入範疇(scope)的概念了,剛剛的變數都是在全域環境執行的,當有Hoisting的情形發生,宣告自然是提升到全域環境最頂端,那如果是區域變數呢?
getName();
function getName() {
console.log(myName);
var myName = 'Bill';
}
結果:
首先要注意的是,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);
}
可能預期的結果:
但正確的結果:
會發生這樣的結果,是因為範疇的規則:
在函式中定義和全域變數同名的區域變數,如此一來,整個函式都看不到同名的全域變數。
因此函式內的程式碼另一種角度來看,變數宣告會提升至範疇的最頂端:
function scope() {
var x;
console.log(x);
x = 'local';
console.log(x);
}
那如果是全域變數與全域函式撞名呢?
var x;
function x() {
console.log('Hello World');
}
console.log(x);
結果:
顯然是函式的優先權大於變數。
如果對x賦值的話,便會蓋過函式拿到優先權:
var x = 'global x';
function x() {
console.log('function x');
}
console.log(x);
如果呼叫函式,會產生錯誤:
var x = 'global x';
function x() {
console.log('function x');
}
x();
即便你對程式的流程瞭如指掌,如果對於Hoisting,這個在JS很特殊的行為不熟悉的話,還是有可能發生意想不到的錯誤,了解此特性,更應該要養成良好的習慣。
讓變數宣告盡可能地靠近被使用的地方,或是宣告在範疇的最頂端。
參考來源:
JavaScript全攻略:克服JS的奇怪部分 執行環境:創造與提升