Jun

只要我的心還會跳,腿還能動

我就沒有理由停下前進的步伐

關於 hoisting


這篇針對常見跟面試可能會出現的問題紀錄一下 JavaScript 的特產 hoisting。

最基本的,在 JavaScript 裡如果試圖對一個尚未宣告的變數取值,那麼你會得到以下錯誤:

console.log(x); // ReferenceError: x is not defined
var

但如果你這樣寫:

console.log(x); // undefined
var x;

得到的卻是 undefined

程式是一行一行由上往下執行的,出現這樣的結果應該挺令人驚訝的,但簡單來講這就是 hoisting。

可以把上面那段程式碼想像成這樣:

var x;
console.log(x); // undefined

我們只能用想像的,因為程式碼本身並不會直接被移動。

console.log(x); // undefined
var x = 100;

這段程式碼我們可以想像成這樣:

var x;
console.log(x); // undefined
x = 100;

JavaScript 實際在執行程式時,會把 var x = 100; 分成兩個步驟

  1. 宣告變數 x

  2. 賦值 100 給變數 x

var x = 100;
var x;
console.log(x); // 100

對於重複宣告,x 的值依然是 100,我們可以這樣想像:

var x;
var x;
x = 100;
console.log(x); // 100

這樣就很好懂了,先別管為什麼 JavaScript 這麼機車,但你總要瞭解的對吧。

console.log(x); // ƒ x() {}
var x;
function x() {}
console.log(x); // ƒ x() {}
function x() {}
var x;

最後一個,無論上面的程式碼順序怎麼擺,你可以看到 function 的宣告提升會優先於變數的宣告提升。

let & const

在 ES6 多了兩個新的變數宣告的關鍵字,letconst,彼此的行為大同小異,我們以 let 為例:

console.log(x); // undefined
console.log(y); // Cannot access 'y' before initialization
var x;
let y;

var 不同在於:

letconst 宣告的變數,當它們的詞法環境被實例化時會被創建,但只有在變數的詞法綁定已經被求值運算後才能夠被訪問。

也就是說當程式進入新的作用域(function,block,module)時,作用域內如果有經由 letconst 宣告的變數,該變數會先在作用域中被創建,但此時還未對宣告語句進行求值運算,所以該變數不得被訪問,硬要訪問就會像上面一樣拋出錯誤。

所以 letconst 也有提升,只是拋出的訊息與 var 不同,var 還會給你一個 undefined 的值,但 letconst 很明顯的直接給你一個錯誤訊息,且這是一個在運行期間才會得到的錯誤訊息。

重點在於以 letconst 宣告的變數或常數,必須經過對宣告的賦值語句進行求值後,才算初始化完成,創建期間並不算初始化。

我們看看這個例子:

let x = 'outer';

(function() {
  // 在這裡 x 的詞法環境被創建,但還未對賦值語句進行求值所以不算初始化完成
  console.log(x); // Cannot access 'x' before initialization
  let x = 'inner';
})();

很明顯與 var 不同在於,letconst 直接不允許你在宣告變數前訪問該變數,如果你這麼做那麼在 run time 會直接噴錯,如果上面的 let 改成 var 那麼 console 出來的就是 undefined。也算是養成一個良好的寫 code 習慣。