前言
先看看這段程式碼
console.log(a)
var a = 10
照 JavaScript 逐行執行的規則,應該會印出 a is not defined
,但這邊卻印出 undefined。
這裡的變數宣告被提升到最上面,這個現象就是 hoisting
如果你只想了解最基本的提升,就差不多是這樣了。
你真的懂 hoisting 嗎?
其實深入去研究 hoisting,才知道有很多概念沒有搞懂。
首先,hoisting 是跟變數有關的,所以他只會發生在他的 scope 裡面
var a = 'global'
function test(){
console.log(a)
var a = 'local'
}
test()
可能會很多人以為印出的是 global
但剛說到 hoisting 只會發生在他的 scope 內,所以你可以把上面那段看成以下:
var a = 'global'
function test(){
var a
console.log(a)
a = 'local'
}
test()
印出的就會是 undefined
-
那再來看一個
function test(){
console.log(a)
var a = 'local'
function a(){
}
}
test()
這邊會印出 function: a
你可能會覺得是順序的問題,但把兩個宣告互換,一樣會得到 function: a,代表 function 會佔有優先權。
那如果有兩個 function 呢?
後面宣告的會蓋掉前面的,很合理。
這邊來說明一下優先順序:
- function
- argument (傳入 function 的參數)
- var
但要注意的是,如果 a 重新宣告
function test(){
var a = '456'
console.log(a)
}
test(123)
答案就會變成 456,因為 a 又重新宣告,並且有值。
所以說沒有值就不會影響嗎? 對!
function test(){
var a
console.log(a)
}
test(123)
會印出 123,而忽略掉沒賦值的 a
Hoisting 運作的規則
那 hoisting 實際上是怎麼運作的呢?
根據 ECMAScript 的規範,每當你進入一個 function 的時候,就會產生一個 EC(Execution Contexts),並且把 EC 放到 stack 中,一層一層疊上去,EC 裡面包含這個 function 的所有資訊。
最上層的是正在運行的 EC,執行完畢便會抽離;最底層的則是 Global Execution Contexts。
那我們剛剛說到 EC 裡面包含這個 function 的所有資訊,指的是什麼呢?
每個 EC 都有 VO(Variable Object),裡面所有宣告的變數或 function 都會變成他的 properties。
以這個程式碼為例:
function test(){
var a = 1
}
他的 VO 就可以看成像這樣的物件
VO: {
a: 1
}
-
當我們 call 這個 function 並帶參數時,他便會幫參數初始化
function test(a, b){
}
test(123)
他的VO就會長成以下這樣:
VO: {
a: 123
b: undefined
}
有傳的參數就會寫入並且賦值,沒傳的就會初始化成 undefined
-
而當我們進入 EC 時,初始化後如果碰到同名的 function 時,這個值就會被取代成這個 function。
function test(a){
function a(){
}
}
test(123)
他的 VO 就會長成這樣
VO: {
a: pointer to function a
}
—
進入 EC,處理完 function 的 case 後,便會開始初始化宣告的變數。
如果這個宣告的變數,在 VO 已經存在且有值,則忽略這個宣告。
如果發現這個宣告的變數 VO 裡面沒有,則初始化這個宣告成 undefined,接下來才開始逐行執行。
let & const 的 hoisting
有發現講了這麼久的 hoisting,卻一直沒提到 let 和 const 嗎?
那是因為他們的機制有些許不同
先看一段程式碼
function test(){
console.log(a)
let a = 10
}
test()
按照之前的邏輯你應該會認為要印出 undefined
但實際上會印出 a is not defined
這是為什麼呢?
難道 let & const 沒有 hoisting 嗎?
其實是有的!一樣可以看成以下
function test(){
let a
console.log(a)
a = 10
}
test()
但是不一樣的是,從進入這個 EC 到 a 被賦值之前,你不能夠去存取 a,否則就會出現錯誤。
而進入這個 EC 到 a 被賦值之前的這個區間,被稱為 TDZ(Temporal Dead Zone)
以上的內容是參考 Huli 大大的文章所寫下的筆記,更詳細、深入的可以參考這篇:我知道你懂 hoisting,可是你了解到多深?