給自己看的 JavaScript 進階 - hoisting
— 程式導師計劃, coding 筆記, JavaScript — 8 min read
給自己看的 JS 進階:(建議按照順序看) 給自己看的 JS 進階-變數 給自己看的 JS 進階-Hoisting 給自己看的 JS 進階-Closure 給自己看的 JS 進階-物件導向
如果只輸入 console.log(b)
會因為 b 沒有被宣告過而噴錯,但如果這樣寫:
console.log(b);var b = 20;
第一行會顯示 indefined
的結果,是因為對 JavaScript 來說,其實是:
var b;console.log(b);b = 20;
這個現象叫做 hoistion
提升,在 JS 中,只有宣告 var b
會被提升,賦值 b = 20
並不會。
function 也會提升:
test(); // 123
function test() { console.log(123);}
值得注意的是,下列寫法會出錯:
test(); // test is not a function
var test = () => { console.log(123);};
因為對 JS 來說,實際上提升的只有宣告,所以是這樣:
var test;test();
test = () => { console.log(123);};
hoisting 的順序
hoisting 只會發生在自己的 scope 中,例如:
var a = 'global';function test() { console.log(a); var a = 'local';}
test();
會印出 undefined
,因為 function 內有個 hoisting,所以實際上是這樣:
var a = 'global';function test() { var a; console.log(a); a = 'local';}
test();
提升的優先順序
function
的提升會佔有優先權:
console.log(a); // [Function a]function a() {}var a = 'a';
可以看成這樣:
function a() {}console.log(a); // [Function a]var a = 'a';
- 後面蓋掉前面的
console.log(a); // 2var a = 1;var a = 2;
- 提升變數不會影響函式輸入的參數
function test(a) { console.log(a); // 123 var a = 456;}test(123);
因為上述提升後只是先定義 a 只是「我要宣告變數 a ㄛ~」沒有影響,但賦值會影響:
function test(a) { var a = undefined; console.log(a); // undefined a = 456;}test(123);
- 提升 function 會被蓋過去
function test() { console.log(a); // [Function a] function a() {}}test(123);
因此可歸納出 hoisting 的優先順序:
- function
- arguments
- var
hoisting 原理
開始之前先試著自己做做看這個題目:
var a = 1;function test() { console.log('1.', a); var a = 7; console.log('2.', a); a++; var a; inner(); console.log('4.', a); function inner() { console.log('3.', a); a = 30; b = 200; }}test();console.log('5.', a);a = 70;console.log('6.', a);console.log('7.', b);
我先猜答案是:
1. 12. 73. 84. 305. 306. 707. b is not defined
我們先 hoisting 成 JS 真正跑的順序好了:
var a = 1;function test() { var a; // hoisting 上來 console.log('1.', a); // 找到上一行,undefined a = 7; console.log('2.', a); // 7 a++; // 此時 a = 8 var a; // 沒有影響,已經有 a 了 inner(); console.log('4.', a); // 可看下三行已經被改成 30 function inner() { console.log('3.', a); // 本身沒有宣告,往上一層找 a = 8 a = 30; // 因為沒有用 var 宣告,因此更改到 test() 中的 a b = 200; // 因為沒有用 var 宣告, b 變成全域變數 }}test();console.log('5.', a); // 和 test scope 無關了,看全域 a = 1a = 70;console.log('6.', a); // 70console.log('7.', b); // inner 的 b 是全域變數,因此是 200
因此答案是:
1. undefined2. 73. 84. 305. 16. 707. 200
接著來看 ECMAScript ES3 的部分
我們一開始再粉紅色的 Global Execution Context
,之後每進入一層函式就堆高一層,結束後就抽掉退出(可以想像玩疊疊樂?或同時看很多本書,最上面的是正在看的,看完就放到一邊),最上面的表示現在所在位置。整個程式結束時會回到最下層。
每個 Execution Context 中都有一個 Variable Object
(VO) ,可以想像成是一個物件,每個變數和值都會對應到 key 和 value 。例如:
var a = 1;
// 這裡的 VO 可以想成VO: { a: 1;}
當進入新的 Execution Context (例如一個 function )時, VO 會自動初始化。順序如下:
- 將參數傳入。
- 傳入 function,就算已經有值也蓋掉。(可以解釋為何 function 順位最高)
- 最後是變數宣告,如果有值就忽略(因此順位最低),沒有的話就增加一個先定義為 undefined 。
之後才會開始跑裡面的 code 。
回頭看剛剛那題:
var a = 1; //1function test() { console.log('1.', a); // 3 var a = 7; // 4 console.log('2.', a); // 5 a++; //6 var a; // 7 inner(); // 8 console.log('4.', a); // 12 function inner() { console.log('3.', a); // 9 a = 30; // 10 b = 200; // 11 }}test(); // 2console.log('5.', a); // 13a = 70; // 14console.log('6.', a); //15console.log('7.', b); //16
一開始進去的時後 global VO
開始初始化:
global VO: { test: function, a: undefined}
global VO
的 a 變成 1- 進入 test() ,新的
test VO
初始化:
test VO: { inner: function, a: undefined}
- 此時的
test VO
中 a 是 undefined ,輸出。 test VO
的 a 變成 7。test VO
的 a 是 7,輸出。test VO
的 a 變成 8 。- 宣告過了,不用理他。
- 進入 inner() ,新的
test VO
初始化:
test VO: { // 沒有任何參數、變數和函式,因此是空的}
inner VO
中沒有 a ,往上找到test VO
中的 a 是 8 ,回傳。inner VO
中沒有 a ,往上找到test VO
改 a 的值為 30 。inner VO
中沒有 b ,往上找test VO
,因此將 b: 200 放在global VO
中(也就是變成全域變數)。inner() 執行結束,抽掉inner EC
。- 因為 10 ,
test VO
中的值為 30 。test() 執行結束,抽掉test EC
。 global VO
的 a 為 1 (可見第一條),回傳 。global VO
的 a 改變成 70 。global VO
的 a 為 70,回傳。global VO
的 b 為 200(可見 11 條),回傳。- 全部執行完,退出
Global EC
。
let 和 const 的 hoisting
先看一個情境:
console.log(a);let a = 20;
結果竟然會噴錯!難道 let 和 const 是沒有 hoisting 的嗎?!
其實 let 和 const 是有 hoisting 的,只是有一些奇怪的限制。我們先將 hoisting 後的結果寫下來:
let a;console.log(a);a = 20;
在使用 let 和 const 宣告變數的時候,在變數被賦值之前都不能被使用,因此才會噴錯。在宣告候到賦值前的區塊,有個詞叫 Temporal Dead Zone
,在區域中不能取用這個值~