何謂 hoisting(向上提升)?以 let、const、var、function 為例

向上提升( Hoisting )指的是 JavaScript 允許函式與變數在宣告之前,就可以叫用而不會出錯的一種情況。

以上就是我本來了解的「向上提升( Hoisting )」,我以為自己懂了,寫這篇筆記查資料時,我才發現自己並不懂。

先來談談變數的「向上提升( Hoisting )」。

變數的向上提升(hoisting)

在 JavaScript 中建立變數包含2個動作:

  • 宣告:就是給變數一個名稱。
  • 初始化:給變數一個初始值。

ES6 的 let 與 const 出現之前,在 JavaScript 中都使用 var 來宣告變數。 程式碼的執行過程中,用var宣告變數可以在前面就先使用,後面才宣告。宣告之前叫用不會出現錯誤,只是會給變數 undefined 的值而已。

console.log(superMan);
//Uncaught ReferenceError: superMan is not defined

console.log(wonderWoman);
//Uncaught ReferenceError: wonderWoman is not defined
wonderWoman = '黛安娜';


console.log(batMan); //undefined  在程式碼的前面呼叫
var batMan = '布魯斯·韋恩'; //後面才宣告賦值

在上面的範例中, superMan 沒有被宣告,所以JavaScript在程式碼中找不到這個變數,直接報錯: Uncaught ReferenceError: superMan is not defined。

wonderWoman 雖然有宣告,但是並沒有使用 var 來宣告,在提前叫用的時候,console.log的結果也是: Uncaught ReferenceError: wonderWoman is not defined。

var宣告的變數只會提升宣告,不會提升賦值

使用var來宣告的變數在JavaScript中有特別待遇,上面的範例用var宣告 superMan 之前,console.log(superMan)並不會報錯,而是出現 ‘undefined’。這是因為JavaScript在執行時發現有沒被宣告的變數,它會先在程式碼中找一找看是否在後面被宣告了,如果有宣告,那就把宣告「提升(Hosting)」到前面。

要注意的是: var 宣告的變數,宣告的部分會提升,但是賦值的部分不會提升,所以 console.log() 時雖然不會報錯,而是會出現 ‘undefined’,呈現一種沒有給值的狀態。

那提升的過程中發生了甚麼事呢?以上面的batMan為例:

console.log(batMan); //undefined
var batMan = '布魯斯·韋恩';

其實是變成這樣:

var batMan; //宣告被提前了
console.log(batMan); //undefined
batMan = '布魯斯·韋恩'; //但是賦值還在後面

var宣告變數的提升(Hoisting)其實就是把宣告與賦值拆成兩個部分,宣告提到前面執行,賦值則還在後面本來的位置上。

but…在《我知道你懂 hoisting,可是你了解到多深?》看到兩個範例,我把它改寫一下,來增強自己的記憶,先看這個:

var batMan = '布魯斯·韋恩';
var batMan;
console.log(batMan); //布魯斯·韋恩

嗯,結果不是 ‘undefined’ 喔,而是 ‘布魯斯·韋恩’!
上面那個例子,還是可以切分為「宣告」與「賦值」這兩部分:

var batMan;
var batMan;
batMan = '布魯斯·韋恩';
console.log(batMan); //布魯斯·韋恩

再來看這個範例:

function batManWeapon(arm){
  console.log(arm);
  var arm = '蝙蝠摩托';
}
batManWeapon('蝙蝠車') // '蝙蝠車'

答案是蝙蝠車喔!

這個過程是這樣的:

function batManWeapon(arm){
    var arm = '蝙蝠車'; //batManWeapon('蝙蝠車')呼叫函式,帶入的參數。
    var arm;  //變數提升
    console.log(arm)
    arm = '蝙蝠摩托'
}
batManWeapon('蝙蝠車') // '蝙蝠車'

這是變數提升(Hosting)需要特別注意的地方。

let與const宣告的變數有被提升的待遇嗎?

我們來看看用let與const宣告的變數是否會有「向上提升」的待遇:

console.log(superMan); //Uncaught ReferenceError: batMan is not defined
let superMan = '克拉克';

console.log(batMan);
const batMan = '布魯斯.偉恩' //Uncaught ReferenceError: batMan is not defined

console.log的結果都是 “Uncaught ReferenceError: batMan is not defined”,這樣我們是否可以說用 let 與 const 宣告的變數沒有「向上提升」的待遇?

再來看一下這個範例:

let superMan = '克拉克';
function superHero(){
    console.log(superMan);     
}
superHero() //'克拉克'

superHero()函式的作用域中沒有宣告superMan,所以函式外的全域環境去找這個變數,找到 let superMan = ‘克拉克’ ,就把這個同名的變數捉進函式內使用。

那如果題目改成這樣呢?

let superMan = '克拉克';
function superHero(){
    console.log(superMan); 
    let superMan = '攝影記者';
}
superHero() //Uncaught ReferenceError: Cannot access 'superMan' before initialization

這一題出現紅字:意思是 superMan 這個變數在初始化之前,無法使用。

這就有一個問題:如果 superHero 函式內的 superMan 變數沒被提升,那應該會去捉函式外部的 superMan = ‘克拉克’ 這個變數,而不是跑出 Uncaught ReferenceError: Cannot access ‘superMan’ before initialization 這樣的紅字結果。

所以JavaScript在執行的時候,一定也有在函式內部找,找到在 console.log 後面有宣告 superMan這個變數,既然作用域裡面有宣告,就不去外面找,所以就給你報錯的結果:Uncaught ReferenceError: Cannot access ‘superMan’ before initialization。

差別只在 用var宣告的變數「提升」時會被賦予 ‘undefined’,但是用 let 與 const 宣告的變數「提升」時卻是紅字報錯,讓程式中斷執行不下去?

暫時死區Temporal Dead Zone

let宣告的變數在尚未賦值之前,不像var一樣會以undefined初始化,所以let與const宣告的變數從宣告到初始化之間,將會無法操作,這段時間稱為「暫時死區」(Temporal Dead Zone)。

const因為宣告時,必須給值,且之後不能再改變,所以沒有TDZ的問題。

函式的向上提升

函式可以分為兩種:

  • 以「函式宣告」定義的函式
  • 函式運算式

其中以「函式宣告」定義的函式,可以在函式宣告前就使用,這就稱為「函式提升」。隨叫隨到,不管身在何方,真的是 JavaScript 裡面的超級英雄。

batMan();//布魯斯‧韋恩

function batMan(){
    console.log('布魯斯‧韋恩')
}

但是,函式運算式在宣告前呼叫函式就會報錯。

batMan(); //Uncaught ReferenceError: batMan is not defined

let batMan = function(){
    console.log('布魯斯‧韋恩')
}

而且函式的提升,不像 var 宣告變數那樣用 ‘undefined’ 暫時充代,而是整個內容都被提升。

為什麼函式需要「向上提升」呢?

之前對於「提升」( Hoisting )這個題目只是硬背了起來,直到這次寫筆記與作 BMI KATA 的練習才恍然大悟。

這是因為方便函式之間彼此呼叫使用,在前面宣告的函式可以去叫後面才宣告的函式來使用。如果沒有「提升」( Hosisting )的話,函式的使用會疊床架屋,十分冗長。

最後來個情境題,假設噗攏共星球的外星人來攻打地球,需要呼叫超級英雄們來幫忙,當然是不管在哪裡呼叫,都能把超級英雄叫來,是最方便的:

crisis ();
//Superman
//Wonder woman
//Batman

function callSuperMan (){
    console.log('Superman');
    callwonderWoman();
}


function crisis (){
    callSuperMan ();
    callBatMan ();
}

function callBatMan (){
    console.log('Batman')
}

function callwonderWoman(){
    console.log('Wonder woman')
}

這樣不管函式在前面還是後面都可以叫的到,還可以在函式內呼叫別的函式。

地球的危機就解除了!

總結

  • var 宣告的變數只會提升宣告,不會提升賦值,所以提升時,值會是 ‘undefined’。
  • let 與 const 宣告的變數,如果在宣告前使用,會報出錯誤。
  • 只有「函式宣告」享有「向上提升」的待遇。
  • 函式的提升是整個內容都被提升,可以在宣告之前就呼叫使用。
  • 函式的提升是為了提供函式之間互相呼叫的便利性。

最後,養成好習慣,變數與函式應該先定義好再呼叫,避免誤用「 hoisting 」這項好設計!

參考資料:


何謂 hoisting(向上提升)?以 let、const、var、function 為例
https://popeye-ux.github.io/2021/11/12/hoisting/
作者
POPEYE
發布於
2021年11月12日
許可協議