何謂 hoisting(向上提升)?以 let、const、var、function 為例
向上提升( Hoisting )指的是 JavaScript 允許函式與變數在宣告之前,就可以叫用而不會出錯的一種情況。
以上就是我本來了解的「向上提升( Hoisting )」,我以為自己懂了,寫這篇筆記查資料時,我才發現自己並不懂。
先來談談變數的「向上提升( Hoisting )」。
變數的向上提升(hoisting)
在 JavaScript 中建立變數包含2個動作:
- 宣告:就是給變數一個名稱。
- 初始化:給變數一個初始值。
ES6 的 let 與 const 出現之前,在 JavaScript 中都使用 var 來宣告變數。 程式碼的執行過程中,用var宣告變數可以在前面就先使用,後面才宣告。宣告之前叫用不會出現錯誤,只是會給變數 undefined 的值而已。
在上面的範例中, 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為例:
其實是變成這樣:
var宣告變數的提升(Hoisting)其實就是把宣告與賦值拆成兩個部分,宣告提到前面執行,賦值則還在後面本來的位置上。
but…在《我知道你懂 hoisting,可是你了解到多深?》看到兩個範例,我把它改寫一下,來增強自己的記憶,先看這個:
嗯,結果不是 ‘undefined’ 喔,而是 ‘布魯斯·韋恩’!
上面那個例子,還是可以切分為「宣告」與「賦值」這兩部分:
再來看這個範例:
答案是蝙蝠車喔!
這個過程是這樣的:
這是變數提升(Hosting)需要特別注意的地方。
let與const宣告的變數有被提升的待遇嗎?
我們來看看用let與const宣告的變數是否會有「向上提升」的待遇:
console.log的結果都是 “Uncaught ReferenceError: batMan is not defined”,這樣我們是否可以說用 let 與 const 宣告的變數沒有「向上提升」的待遇?
再來看一下這個範例:
superHero()函式的作用域中沒有宣告superMan,所以函式外的全域環境去找這個變數,找到 let superMan = ‘克拉克’ ,就把這個同名的變數捉進函式內使用。
那如果題目改成這樣呢?
這一題出現紅字:意思是 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 裡面的超級英雄。
但是,函式運算式在宣告前呼叫函式就會報錯。
而且函式的提升,不像 var 宣告變數那樣用 ‘undefined’ 暫時充代,而是整個內容都被提升。
為什麼函式需要「向上提升」呢?
之前對於「提升」( Hoisting )這個題目只是硬背了起來,直到這次寫筆記與作 BMI KATA 的練習才恍然大悟。
這是因為方便函式之間彼此呼叫使用,在前面宣告的函式可以去叫後面才宣告的函式來使用。如果沒有「提升」( Hosisting )的話,函式的使用會疊床架屋,十分冗長。
最後來個情境題,假設噗攏共星球的外星人來攻打地球,需要呼叫超級英雄們來幫忙,當然是不管在哪裡呼叫,都能把超級英雄叫來,是最方便的:
這樣不管函式在前面還是後面都可以叫的到,還可以在函式內呼叫別的函式。
地球的危機就解除了!
總結
- var 宣告的變數只會提升宣告,不會提升賦值,所以提升時,值會是 ‘undefined’。
- let 與 const 宣告的變數,如果在宣告前使用,會報出錯誤。
- 只有「函式宣告」享有「向上提升」的待遇。
- 函式的提升是整個內容都被提升,可以在宣告之前就呼叫使用。
- 函式的提升是為了提供函式之間互相呼叫的便利性。
最後,養成好習慣,變數與函式應該先定義好再呼叫,避免誤用「 hoisting 」這項好設計!
參考資料: