變數宣告

宣告是為了告訴電腦需要一個記憶體空間來存放資料,類似置物櫃的概念。

let / const

let 的內容可以隨時賦值 (assign) 改變,宣告時可以不賦值,內容為 undefined

const 指不會變動的內容 (constant),宣告後必須賦值且不能改變內容,否則會報錯。

let a; // a 為 undefined

const b; // 報錯 const 在宣告時必須賦予值

const c = 1;
c = 2; // 報錯 不能改變 const 的內容

var

ES6 前只有 var 這個宣告方式,但語法規定寬鬆,容易造成撰寫上的意外。

主要有以下機制:

  1. 可以重複宣告:

有碰過其他語言的話一定會覺得重複宣告是非常奇怪的事:

var a = 1;

console.log(a); // 1

var a = '我被重新宣告竟然沒有報錯太扯了吧';

console.log(a); // '我被重新宣告竟然沒有報錯太扯了吧'
  1. 作用域

letconst 的作用區間以大括號為限,離開大括號後就無法存取,包含迴圈、函式,或隨手寫一組大括號等都符合這個規則:

{
  let a = 1;
  const b = 'hello';
}

console.log(a, b); // 報錯,a 與 b 未宣告

大部分的語言也是遵循這種以大括號為存取限制的概念設計的,但是 var 的行為不同:

var a = 1;

for (var i = 1; i < 5; i++) {
  a += i;
}

console.log(a); // 11
console.log(i); // 5

迴圈跑完後可以查看 a 的值是 11,這是正確的,但是連 i 的值都可以存取,這是因為 var 的作用域是在函式內部,而 for 的迭代只是一組大括號而已,並不是函式。

以上特性都有機率產生全域污染,例如取出預期之外的值或是無意間修改了同名的資料,所以現今撰寫 JS 時幾乎不推薦使用 var 宣告。

必考題

for (var i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i);
  }, 0);
}

最終會印出 3 次 3

setTimeout 是一個非同步的 Web API,在 JS 的執行環境 (runtime) 中呼叫這類的 API 時會先被放到另外一個待處理的空間,和 setTimeout 後面給予的時間參數 0 無關,原生的運算行為會優先執行完畢,所以迴圈會繼續迭代完才進行 setTimeout 的內容:

var i = 0;

{
  i++; // i 為 1
}
{
  i++; // i 為 2
}
{
  i++; // i 為 3,迴圈終止
}

console.log(i);
console.log(i);
console.log(i);
資訊

Web API 執行順序與事件循環(Event Loop)有關。


變數一定要經過宣告嗎

沒有任何宣告的關鍵字,直接寫出變數名稱,會變成全域可以存取的值:

function foo() {
  a = 1;
}

foo();

console.log(a); // 1
console.log(window.a); // 1

foo 執行完後仍然可以找到 a,此時 a 是全域物件 window 的一個屬性 (property),用 window.a 可以取出來,所以嚴格來說 a 目前不是一個獨立的變數。

屬性與變數最重要的差異在於,屬性可以使用 delete 運算子刪除:

function foo() {
  a = 1;
}

foo();

delete a;
console.log(a); // Uncaught ReferenceError: a is not defined

let b = 2;
delete b;
console.log(b); // 2

delete a 之後印出 a 會報錯,因為 a 是屬性,已經確實被刪除。

而使用 let 宣告的 b 還是可以印出來,因為 delete 運算子對變數不生效(但是不會報錯)。


小結

varletconst
作用域函式大括號大括號
初始值非必須非必須必須
重複宣告可以不行不行
  • var 的作用域和 for 迴圈的相關問題
  • 未使用關鍵字宣告的變數會變成全域物件的屬性
  • delete 只對屬性生效

參考資料