hoisting 只會提升宣告而非賦值
console.log(b)
var b = 10
// 輸出 undefined,而非 error
-----// 上面這段可以解讀成下面
var b // 宣告變數
console.log(b)
b = 10 // 賦值
上下半部是相同意思,因為在==宣告變數的部分會被提升 hoisting==,而賦值不會,所以在經過程式碼由上而下執行之後,就不會有b is undefined
的錯誤訊息,只是沒有賦值。
較常見的例子是 function 的使用,可以在 function 宣告之前就先用這個 function,再放置的順序上可以隨意。
test()
function test() {
console.log(123)
}
-----// 上面這段可以解讀成下面
function test() {
console.log(123)
}
test()
上下兩段是相同的,可以解讀成==整個 function 都被提升到最上面==,所以可以順利呼叫
但是當 function 是以變數宣告時
test()
var test = function() {
console.log(123)
} // test is not a function
----// 上面這段可以解讀成下面
var test // 只有 var 會提升到最上面
test()
test = function() { // 賦值會留在最下面
console.log(123)
}
上下兩段一樣意思,var test = function() {
console.log(123)
}
這段會被拆成宣告變數與賦值兩部分,因為只有 var 會提升到最上面,所以呼叫test()
會是錯誤訊息
function 宣告、function 的參數以及一般變數宣告同時出現時的提升優先順序
- function
- arguments 參數
- var
註:如果宣告兩個同名的 function 或變數,後面的會取代前面的。
function test(v){
console.log(v)
var v = 3
}
test(10)
----
function test(v){
var v = 10 // 因為下面呼叫 test(10)
var v
console.log(v)
v = 3
}
test(10)
以上輸出是 10,參數會比一般變數宣告優先提升
console.log(a) //[Function: a]
var a
function a(){}
除了變數宣告以外,function 的宣告也會提升而且優先權比較高,因此上面的程式碼會輸出 function
而不是 undefined
。
小測驗
var a = 1;
function test(){
console.log('1.', a); // undifined,因為 var a 提升
var a = 7;
console.log('2.', a); // 7
a++;
var a;
inner();
console.log('4.', a); // 30
function inner(){
console.log('3.', a)// 8
// 在 inner 沒有 a,
// 往上層找到 a 是 8
a = 30;
b = 200; // 往上層找都沒有 b, 就變成全域變數
}
}
test();
console.log('5.', a); // 1
a = 70;
console.log('6.', a); // 70
console.log('7.', b); // 200
Temporal dead zone:let 與 const 的 hoisting 行為
let a = 10
function test() {
console.log(a) // undefined
let a = 30
}
test()
----
let a = 10
function test() {
let a // 跟 var 一樣會提升
console.log(a) // error undefined
a = 30
}
test()
- 在變數被賦值之前,中間過程都不能存取變數,不然會發生錯誤。==而進入函式(提升之後)到賦值的這段期間叫 temporal dead zone==,這段區間內都不能存取變數的值。
- let 與 const 也有 hoisting 但沒有初始化為 undefined,而且在賦值之前試圖取值會發生錯誤。
hositing 運作原理
Execution Contexts
- 以下用 ES3 的規則為例
每當你進入一個 function 的時候,就會產生一個 EC,裡面儲存跟這個 function 有關的一些資訊,並且把這個 EC 放到 stack 裡面,當 function 執行完以後,就會把 EC 給 pop 出來。
簡而言之,所有 function 需要的資訊都會存在 EC,也就是執行環境裡面,你要什麼都去那邊拿就對了。
示意圖大概就像這樣,要記得除了 function 有 EC 以外,還有一個 global EC:
每個 EC 都會有相對應的 variable object(以下簡稱 VO),在裡面宣告的變數跟函式都會被加進 VO 裡面,如果是 function,那參數也會被加到 VO 裡。可以把 VO 想像成就是一個 JavaScript 的物件就好。
而 VO 在存取值的時候會用到,例如說 var a = 10 這一句,之前有講過可以分成左右兩塊:
var a
:去 VO 裡面新增一個屬性叫做 a(如果沒有 a 這個屬性的話)並初始化成 undefineda = 10
:先在 VO 裡面找到叫做 a 的屬性,找到之後設定為 10
這邊如果 VO 裡面找不到怎麼辦?它會透過 scope chain 不斷往上尋找,如果每一層都找不到就會拋出錯誤。
當我們在進入一個 EC 的時候(你可以把它想成就是在執行 function 後,但還沒開始跑 function 內部的程式碼以前),會按照順序做以下三件事:
- 把參數放到 VO 裡面並設定好值,傳什麼進來就是什麼,沒有值的設成 undefined
- 把 function 宣告放到 VO 裡,如果已經有同名的就覆蓋掉
- 把變數宣告放到 VO 裡,如果已經有同名的則忽略
範例:
function test(v){
console.log(v)
var v = 3
}
test(10)
每個 function 你都可以想成其實執行有兩個階段,第一個階段是進入 EC,第二個階段才是真的一行行執行程式。
在進入 EC 的時候開始建立 VO,因為有傳參數進去,所以先把 v 放到 VO 並且值設定為 10,再來對於裡面的變數宣告,VO 裡面已經有 v 這個屬性了,所以忽略不管,因此 VO 就長這樣子:
{
v: 10
}
進入 EC 接著建立完 VO 以後,才開始一行行執行,這也是為什麼你在第二行時會印出 10 的緣故,因為在那個時間點 VO 裡面的 v 的確就是 10 沒錯。
如果你把程式碼換成這樣:
function test(v){
console.log(v) // 10
var v = 3
console.log(v) // 3
}
test(10)
那第二個印出的 log 就會是 3,因為執行完第三行以後, VO 裡面的值被換成 3 了。
有關 hositing 運作原理與 JS 引擎怎麼運作的詳細介紹,詳見參考資料:我知道你懂 hoisting,可是你了解到多深?