回答一題 var / let / const 就驚艷你的面試官 ─ 語法特性篇

「請解釋 var / let / const 的差別?」

一道經典的前端面試題,告訴我,你的回答是不是只有下列幾種:
var 是 function scope; let / const 是 block scope。」
var 可以被重複宣告; let / const 不行。」
const 宣告以後就不能重新賦值。」

最近在準備面試的過程中,我發現這個題目看似簡單,但其實包含著超級多 JS 的基本概念,對面試官來說,follow up 可以延伸到各個不同的議題如:hoisting, variable life cycle, GC, scope and context 等比較有鑑別度的題目,如果只回答得出上面的答案實在有點可惜。

深入展開以前,先讓我們複習一下變數基本特性吧!畢竟要想考到滿分,得先及格。 (這什麼廢話 😝)

var

  • 在全域宣告的變數 var,也會同時作為全域物件(前端為 window,後端為 global)的屬性
var a = 0;
console.log(window.a); // 0
  • var 可重複宣告,以後宣告的為主。(指的是在同一個作用域底下)
var a = 0;
var a = 1;
console.log(window.a); // 1
  • 不同作用域的情況,在 function 裡面宣告的就只存在於該 function scope。
var a = 0;
function test() {
  var a = 1;
  console.log(a);
}
test(); // 1
console.log(a); // 0
  • 順帶一提,下面這個範例你認為輸出為何?
function test() {
  a = 1;
}
test();
console.log(a);

答案是: 1,原因是在非嚴格模式下,a = 1 若找不到賦值的變數 a則會直接被"隱含式宣告"成全域變數。
注意: 如果採用隱含式宣告是可以被 delete 刪除的,因為它視為幫 windows 新增屬性,相對的其他宣告方式皆無法被 delete

  • 但如果多加一行 var a...
function test() {
  a = 1;
  var a;
}
test();
console.log(a);

答案就會變成: Uncaught ReferenceError: a is not defined因為這次在 function scope 底下找到 var 變數去賦值了(var 有 hoisting),所以 a 不會變成全域變數。

let

  • 作用於Block Scope
if (true) {
  let a = 0;
}
console.log(a); // Uncaught ReferenceError: a is not defined
  • Block Scope底下有宣告動作,則同名變數不得再宣告前賦值 (這個錯誤訊息怪怪的,下面會詳細解釋。)
var a = 123;
if (true) {
  a = 456; // Uncaught ReferenceError: Cannot access 'a' before initialization
  let a;
}

上面這種在宣告前賦值失敗的情況也稱作暫時性死區 TDZ(Temporal Dead Zone)

  • 不可重複宣告
let a = 123;
let a = 456; // Uncaught SyntaxError: Identifier 'a' has already been declared

const

let 的特性都有, 新增以下:

  • 宣告後就不可更改
const a = 123;
a = 456; // Uncaught TypeError: Assignment to constant variable.
  • 也不可單獨宣告不給值

    ECMA 262 15th: It is a Syntax Error if Initializer is not present and IsConstantDeclaration of the LexicalDeclaration containing this LexicalBinding is true.

const a; // Uncaught SyntaxError: Missing initializer in const declaration
  • 同個作用域底下,不可以重複宣告 const
var a = 123;
const a = 456; // Uncaught SyntaxError: Identifier 'a' has already been declared
  • 不同作用域底下則可以重複宣告
var a = 0;
function test() {
  const a = 1;
  console.log(a);
}
test(); // 1
console.log(a); // 0
  • 指向Object或是Array的時候更改其屬性不會報錯(store by reference 所以只確保指標沒有變)
const foo = { a: 0 };
foo.b = 1; // ok
foo = {}; // Uncaught TypeError: Assignment to constant variable.
const foo = [0];
foo.push(1); // ok
foo = []; // Uncaught TypeError: Assignment to constant variable.

接著開始上點強度

  • 為何同樣的例子 var 不會噴錯?
console.log(a); // undefined
var a = 123;
console.log(a); // Uncaught ReferenceError: a is not defined
let a = 123;

MDN: In ECMAScript 2015, let bindings are not subject to Variable Hoisting, which means that let declarations do not move to the top of the current execution context.

聽起來是因為 varhoistinglet 沒有。

等等,你確定?看看剛才這個例子:

var a = 123;
if (true) {
  a = 456; // Uncaught ReferenceError: Cannot access 'a' before initialization at <anonymous>:3:7
  let a;
}

注意:如果把 let 刪掉是不會報錯的

var a = 123;
if (true) {
  a = 456; // ok
}

而且最詭異的點是它竟然報錯在第三行,如果 JS 是逐行執行的,那到第三行 a = 456 為止應該不會出錯才對。

可見 JS interpreter 跟我們想得不太一樣,要能解釋這個問題,我們只能理解 JS interpreter 有對 let 做一定程度的 hoisting ,不然沒道理在還沒執行到第四行以前就影響了第三行的賦值行為,那到底為什麼官方會否認?這背後又有什麼愛恨情仇糾葛不清呢?還請各位客倌先別急躁,敬待小弟下回分曉。

下集:

results matching ""

    No results matching ""