You are currently viewing [筆記]-JavaScript 提升(Hoisting)是什麼?關於提升的5個觀念

[筆記]-JavaScript 提升(Hoisting)是什麼?關於提升的5個觀念

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的屬於函式作用域嗎?
如果忘記了可以透過下面的影片快速複習一下!

var let const之間的差異

就算將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);

總結下函式、參數與變數提升之間的順序:

  1. 函式宣告
  2. 傳入函式的參數
  3. 變數宣告

試著練習一下,提升的順序關係

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: 变量提升和函数提升

如果對文章內容有任何問題或是錯誤,歡迎在底下留言讓我知道: )

發佈留言