JavaScript 是個奇怪的語言,它的變數跟函式可以在程式中任何地方宣告,而且還可以先使用變數在宣告!這跟我在寫C時有很大的區別!
深入了解後,才知道是提升(Hoisting)的影響。至於提升(Hoisting)是什麼?讓我在文章中帶你了解關於提升(Hoisting)的幾個問題:
- 提升是什麼?
- 提升影響了什麼?
- 提升帶來了什麼?
接下來在這裡幫你整理成5個觀念,幫助你了解提升(Hoisting)的概念!
提升是什麼?
首先,看看下面的例子
console.log(num); // undefined
var num = 6;
console.log(num); // 6
上面的例子可以看到變數在宣告之前使用,程式非但沒有報錯,並且得到undefined!但為啥變數的值不是6呢?
這之間的現象便是本文的主題”提升”拉~
提升(Hoisting)是Javascript的一個概念。
概念是:Javascript 在程式的編譯階段,會先把宣告的變數和函式放在程式的頂端,等到實際執行時在賦予其值。
這邊注意提升是邏輯上的概念,並非實際將程式移動到頂端!
因此例子中,在編譯階段會將num提升到程式頂端宣告,但尚未賦值,此時會印出undefined,等程式執行到 var num = 6時才會賦值。
具體可以想成這樣:
var num; // 編譯階段 宣告 num
console.log(num); // 執行階段開始 印出undefined
var num = 6; // num 賦值 6 (初始化)
console.log(num); // 印出 6
接下來看看提升分別在JavaScript中影響了什麼
變數提升
var變數的提升
讓我們從例子來看
例子:
function variableHoisting_V1() {
num = 10;
console.log(num); // 10
var num;
}
variableHoisting_V1();
上面的例子如果程式是由上到下執行,在一般認知下無法印出num的值,
由於提升(hoisting)使得num的宣告被拉到函式頂端,所以可以印出num的值。
這個例子可以等同於下面的寫法:
function variableHoisting_V2() {
var num;
num = 10;
console.log(num); // 10
}
variableHoisting_V2();
還記得變數宣告文章中提到var的屬於函式作用域嗎?
如果忘記了可以透過下面的影片快速複習一下!
就算將num宣告在if中,var宣告的變數依然會提升到整個函式的頂端,所以可以得到相同的結果。
function variableHoisting_V3() {
num = 10;
console.log(num); // 10
if (true) {
var num;
}
}
variableHoisting_V3();
提升有一點要特別注意:變數的宣告會被提升,但變數的賦值不被提升。
下面的例子中,我們知道變數foo被提升了,但是回傳的值不是hello而是undefined,
而是等程式執行到foo=’hoisting’這行才賦值給foo。
variableHoisting_V4();
function variableHoisting_V4() {
console.log(foo); // undefined
var foo;
foo = "hello";
console.log(foo);
}
variableHoisting_V4();
這邊要注意!
foo並不是真的在底層執行時被搬到程式碼的最前面,只是看起來很像是被搬到最上面。
變數和函式的宣告會在編譯階段就被放入記憶體,但實際位置和程式碼中完全一樣。
foo是在編譯階段就被放入記憶體,但實際位置與程式中的位置一致,只是看起來像是被搬到最上面。
接下來還有 ES6 新增的 let 跟 const 沒有講。
Temporal dead zone(暫時死區)與const、let
竟然 var 有提升,那 let/const 有嗎?
來看看下面的例子:
console.log(foo); // Uncaught ReferenceError: Cannot access 'foo' before initialization
let foo;
執行結果看似沒有被提升,反而出現ReferenceError,難道let/const沒有提升?
其實const與let會被提升,只是ES6中let/const宣告的變數被賦值之前不允許使用。
具體可以參考Huli文章範例以及說明:
var a = 10;
function test(){
console.log(a);
let a;
}
test();
我知道你懂 hoisting,可是你了解到多深?
如果 let 真的沒有 hoisting 的話,答案應該會輸出10
,因為 log 那一行會存取到外面的var a = 10
的這個變數
答案卻是:ReferenceError: a is not defined
。
意思就是,它的確提升了,只是提升後的行為跟 var 比較不一樣,所以乍看之下你會以為它沒有提升。
如果在let/const變數宣告前讀取變數,會引發ReferenceError。
而從程式開始執行到let/const宣告的變數之間的區域便被稱為Temporal dead zone(暫時死區),簡稱TDZ。
let/const的TDZ特性讓我們養成先宣告變數,再使用的好習慣。
幫助我們提高程式的可讀性,使程式更容易維護,這也是在變數宣告文章中建議使用let/const的原因!
基本的 hoisting 概念就到這邊告一段落了,在這邊幫你整理一下:
- 只有宣告的變數會提升,賦值不會提升
- let/const宣告的變數也有提升
函式提升
函式的宣告也有提升,而且開發過程中我們經常遇到。
function foo() {
hoistingFunction(); // function hoisting!
}
function hoistingFunction() {
console.log('function hoisting!');
}
foo();
既然函式與變數都有提升,那麼當2者出現在程式中他們又會如何相互影響?
接下來讓我們一起來看函式提升與變數提升的順序關係。
提升的順序關係
函式的提升優先於變數的提升
讓我們從例子來了解!
console.log(foo); // ƒ foo() { }
var foo;
function foo() { };
例子中可以看出底下的函式提升的優先權比變數提升還高,因此印出ƒ foo() { }。
如果宣告重複的函式或變數,會依順序來提升比較後面宣告的函式或變數。
function foo() {
test();
function test() {
console.log(1);
};
function test() {
console.log(2);
};
}
foo(); // 2
變數賦值優先於函式的宣告
那如果宣告的變數有賦值會是怎樣的情形?
function foo() {
console.log(a); // f a() { console.log(a); }
var a = 'hello';
console.log(a); // hello
function a() {
console.log(1);
};
}
foo();
可以看到函式的提升依然優先於變數提升。
由於賦值不會被提升,所以當程式執行到var a = ‘hello’;時,再賦值給a,最終印出hello。
函式傳入的參數也有提升
這是一個容易讓人忽略的地方,當函式有參數傳入時,這個參數也有提升,讓我們以例子來了解吧~
function foo(a) {
var a; // 123
console.log(a);
var a = 2;
}
foo(123);
當函式傳入參數時,參數a已經被宣告,並且優先於函式內部的變數宣告,實際上可以看成下面的例子:
function foo(a) {
var a = 123; // 傳入的參數
var a;
console.log(a);
var a = 2;
}
foo(123);
總結下函式、參數與變數提升之間的順序:
- 函式宣告
- 傳入函式的參數
- 變數宣告
試著練習一下,提升的順序關係
var a = 'global';
function test() {
console.log('Q1:', a);
var a = 'local';
console.log('Q2:', a);
foo1();
console.log('Q4:', a);
foo2(b);
function foo1() {
console.log('Q3:', a);
a = 10;
b = 50;
}
function foo2(b) {
console.log('Q5:', b);
b = 2;
console.log('Q6:', b);
}
}
test();
console.log('Q7:', a);
console.log('Q8:', b);
- Q1 : undefined
- 因為a由底下變數宣告var a 提升而來,此時的a作用域屬於test()
- Q2 : local
- 因為上面對變數a賦值,輸出為local
- Q3 : local
- foo1()中的a屬於無宣告變數,因此會向外層尋找,然後在test()中找到宣告的a,此時a的值為local
- Q4 : 10
- 由於執行foo1(),變數a被重新賦值,所以輸出10
- Q5 : 50
- 變數b為全域變數,執行foo2()時傳入的foo1()的b,所以輸出50,此時的b為全域變數
- Q6 : 2
- 將foo2()的b重新賦值為2
- Q7 : global
- 這裡的a屬於全域變數,而test()的a作用域屬於test()
- Q8 : 50
- foo1()的b因為無宣告,且foo1()到全域之間沒有b被宣告,使b成為全域變數,而foo2()的b作用域屬於foo2()
提升(Hoisting)影響了什麼需要?
提升為我們帶來了什麼?
- 變數可以在宣告前使用
- 讓函式可以彼此互相呼叫
變數可以在宣告函式前使用
JavaScript因為提升的特性,使它有別於沒有提升概念的程式語言如:C語言,JavaScript的變數不需要先宣告便可以使用。
但這樣容易養成事後補上變數的習慣,JavaScript可以無宣告變數,這些忘了宣告的變數容易使程式出現不預期的錯誤,也降低程式的可讀性與可維護性。
因此不建議把提昇當成方便的語法特性使用,而是要養成先宣告,再使用的習慣。
讓函式可以彼此互相呼叫
這點解決了函式需要先宣告才能使用的問題。讓我們可以更輕易地使用函式。
如果沒有提升,我們必須先把函式宣告於檔案的最上方才能在後續檔案中使用函式,並且沒法使函式間互相呼叫。
總結
讓我們總結下目前為止學到的東西:
- 只有宣告的變數會提升,賦值不會提升
- let/const宣告的變數也有提升,但受到TDZ的影響,變數需要在宣告後才能使用
- 函式提升的優先權高於變數提升
- 重複宣告同名函式時,後者會覆寫前者的宣告
- 函式、參數與變數提升之間的順序
- 1.函式宣告
- 2.傳入函式的參數
- 3.變數宣告
提升(Hoisting)帶來的影響
- 變數可以在宣告前使用 (不建議使用這些技巧,會降低程式可讀性與可維護性)
- 讓函式可以彼此互相呼叫
了解提升(Hoisting)可以幫助我們更加了解JavcaScript,但開發過程中我們應該要注重程式的可讀性與可維護性。
因此不應透過提升的特性來寫程式,而是注重程式的可讀性與可維護性。
因此:
- 養成先宣告,再使用的習慣
- 使用let/const替代var,以減少未宣告變數導致不預期錯誤的情形
希望這篇文章可以幫助你更加了解提升的概念以及提升在JavaScript中的用途。
參考資料
JavaScript – The Complete Guide
MDN-Hoisting
我知道你懂 hoisting,可是你了解到多深?
JavaScript: 变量提升和函数提升
如果對文章內容有任何問題或是錯誤,歡迎在底下留言讓我知道: )