JavaScript 的 var、let 與 const
Tags: javascript
Published: 2022-06-13 10:28:59

JavaScript 因為歷史悠久, 所以你可能會遇到經過多人更改過的 JavaScript 程式, 裡面混雜了不同時期 JavaScript 的語法, 導致有時候 JavaScript 程式就是難懂, 本文將針對宣告變數的幾種方法加以說明, 期望能讓大家快速理解其中的差別。

## 使用 var 宣告函式層級的變數

過去最常看到的變數宣告方是就是使用 [`var`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/var), 它宣告的變數是函式層級, 也就是只要離開宣告時所在的函式, 這個變數就失效了, 例如：

```javascript
function foo() {
  var a = 23;
  console.log(a);
}

foo();
console.log(a);
```

執行結果如下：

```
23
Uncaught ReferenceError: a is not defined
```

由於 `a` 是在函式 `foo` 中宣告, 所以叫用 `foo` 函式時可以取用變數 `a`, 但是在函式外取用變數 `a` 就會引發未定義識別字的錯誤。

### 全域變數

如果在函式外使用 `var` 宣告變數, 它就會變成全域變數, 在程式內任何地方都可以取用, 例如：

```javascript
var a = 23;

function foo() {
  console.log(a);
  a = 24;
}

foo();
console.log(a);
```

執行結果如下：

```
23
24
```
### 宣告與設定初值分離

你也可以把宣告和設定初值分開來, 不一定要同時完成, 像是這樣：

```javascript
var a;
a = 23;
console.log(a);
```

如果你希望在同一個地方集中宣告變數, 但是在要用到該變數的時候才設定值, 這樣的寫法就會很有用。

### 重複宣告

你也可以重複宣告同一個變數, 只要沒有設定新的值, 就會保留原值, 例如：

```javascript
var a = 23;
console.log(a);
var a;
console.log(a);
var a = 24;
console.log(a);
```

執行結果如下：

```
23
23
24
```

第三行雖然重新宣告 `a`, 但是沒有重新設定值, 所以 `a` 仍然是 23。

### 變數提升 (variable hoisting)

變數有一個我其實不知道有什麼用途, 但是很多人喜歡拿來考別人的功能, 叫做 [`變數提升 (variable hoisting)`](https://developer.mozilla.org/en-US/docs/Glossary/Hoisting#variable_hoisting), 會把宣告變數的動作提升到執行其他程式前先完成, 意思就是在進入變數的有效範圍內時, 會在執行第一行程式前就先宣告變數。因此, 在執行第一行程式的時候, 變數就已經存在了。例如：

```javascript
console.log(a);
var a = 23;
console.log(a);
```

執行結果如下：

```
undefined
23
```

由於在執行第一行程式前, 就會先宣告變數, 因此第一行程式並不會引發變數尚未宣告的錯誤。不過對於以 `var` 宣告的變數, [**變數提升**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/var#var_hoisting)只會先宣告變數, 並不會執行設定初值的程式, 以上例來說就是不會執行 `a = 23`, 而是設定初值為 `undefined`, 所以你會看到第一行印出 `a` 的值是 `undefined`。等執行到第二行才會設定變數 `a` 的值為 23, 因此第三行就會印出 23 了。

### 全域變數會成為全域物件的屬性

以 `var` 宣告的全域變數會成為全域物件的屬性, 例如：

```javascript
var a = 23;
console.log(globalThis.a);
```

執行結果如下：

```
23
```

不過這個屬性是[**不可設定 (non-configurable)** ](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#data_property) 的, 也就是不能使用 [`delete`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/delete) 移除, 例如以下的程式雖然不會發生錯誤, 但是 `delete` 卻沒有作用：

```javascript
var a = 23;
delete a;
delete globalThis.a;
console.log(a);
```

執行結果 `a` 仍然存在, 印出的值也是正確的：

```
23
```

如果採用嚴格模式, 就會看到錯誤訊息：

```javascript
'use strict';

var a = 23;
delete globalThis.a;
console.log(a);
```

執行結果如下：

```
Uncaught TypeError: property "a" is non-configurable and can't be deleted
```

## 使用 let 宣告區塊層級的變數

所謂的區塊, 就是由一對大括號括起來的區域, 使用 [`let`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let) 宣告的變數只要出了所在的區塊, 就會失效, 例如：

```javascript
{
  let a = 23;
  console.log(a);
}

console.log(a);
```

執行結果如下：

```
23
Uncaught ReferenceError: a is not defined
```

由於第二次取用 `a` 時已經離開了宣告變數的區塊, 因此變數 `a` 已經失效, 就會引發未定義識別字的錯誤。

### 分辨區塊

在大部分的情況下, 我們很容易辨識區塊, 不過在像是 `for` 的敘述中, 初始設定也是區塊的一部份, 因此在初始設定內宣告的變數在 `for` 結束後也一樣會失效, 例如：

```javascript
for(let i = 0;i < 2;i++)
{
  console.log(i);
}

console.log(i);
```

執行結果如下：

```
0
1
Uncaught ReferenceError: i is not defined
```

如果改用 `var` 宣告變數, 由於並沒有離開函式範圍, 所以不會引發錯誤, 例如：

```javascript
for(var i = 0;i < 2;i++)
{
  console.log(i);
}

console.log(i);
```

執行結果最後會印出迴圈結束時的 `i` 值：

```
0
1
2
```

### 全域變數

在任何區塊外使用 `let` 建立的變數一樣是全域變數, 可在程式中任何地方取用, 例如：

```javascript
let i = 0;

for(i = 0;i < 2;i++) {}

console.log(i);
```

結果如下：

```
2
```

### 不能重複宣告變數

使用 `let` 宣告的變數不能重複宣告, 例如：

```javascript
let a = 23;
console.log(a);
let a;
```

執行時會直接引發錯誤告訴你 `a` 已經宣告過了：

```
SyntaxError: Identifier 'a' has already been declared
```

### 變數提升不會設定初值

以 `let` 宣告變數也一樣具有變數提升功能, 但是並不會設定初值, 在使用變數前一定要先透過 `let` 宣告, 例如：

```javascript
console.log(a);
let a = 0;
```

執行時就會引發錯誤：

```
Uncaught ReferenceError: can't access lexical declaration 'a' before initialization
```

表示不能在設定初值前就取用已宣告的變數 `a`。

### 以 let 宣告的變數不會成為全域物件的屬性

以 `let` 宣告的變數並不會像是以 `var` 宣告的變數那樣成為全域物件的屬性, 例如：

```javascript
let a = 23;
console.log(globalThis.a);
```

執行時印出的並不是 `a` 的值, 而是 `undefined`：

```
undefined
```

表示全域物件中並沒有 `a` 這個屬性。

## 使用 const 宣告不能變更的變數

你也可以使用 [`const`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/const) 宣告變數, 不過這種變數如同 `const` 字面所示, 是不能變的, 中文翻譯為**常數**。先來看看以下的例子：

```javascript
const a = 23;
console.log(a);
a = 24;
```

執行後如下：

```
23
Uncaught TypeError: invalid assignment to const 'a'
```

在第三行嘗試設定常數內容時就會引發錯誤, 告訴你不能設定以 `const` 宣告的常數。

### 宣告常數時一定要設定值

以 `const` 宣告常數時必須一併設定初值, 不能將宣告與設定初值分開進行, 像是以下的例子就會引發錯誤：

```javascript
const a;
a = 23;
console.log(a);
```

執行結果如下：

```
Uncaught SyntaxError: missing = in const declaration
```

錯誤訊息告訴我們在 `const` 宣告時少了設定初值的 `=`。

### 變更常數所參照的物件

請特別注意, 不能變更以 `const` 宣告的常數指的是不能重新設定常數本身, 如果常數的內容是一個物件, 你還是可以變更物件內的屬性, 例如：

```javascript
const a = {name: "John"};
a.name = "Mary";
console.log(a);
```

執行結果如下：

```
Object { name: "Mary" }
```

由於變更的是物件的內容, 而不是變更常數 `a`, 所以可以成功執行。同樣的道理, 如果常數的內容是一個陣列, 也可以變更陣列內的項目：

```javascript
const a = [1, 2, 3];
a.push(4);
a[0] = 10;
console.log(a);
```

執行結果如下：

```
Array(4) [ 10, 2, 3, 4 ]
```

除了不能重新設值外, `const` 跟 `let` 是一樣的。

## 沒有宣告直接設定變數

JavaScript 是很寬鬆的, 你甚至會看到有些程式中根本[沒有宣告就直接設定變數的值](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/var#unqualified_identifier_assignments), 像是這樣：

```javascript
a = 23;
console.log(a);
```

執行時並不會引發錯誤, 而且可以正確印出 `a` 的值：

```
23
```

你甚至還可以隨意在函式或是區塊直接用同樣的方式運作：

```javascript
function foo() {
  a = 23
}

{
  b = 24
}

foo()
console.log(a);
console.log(b);
```

執行結果如下：

```
23
24
```

你會看到 `a` 和 `b` 雖然是在函式以及區塊內設定, 可是不像是 `var` 或是 `let` 有範圍的限制, 兩個都變成全域變數那樣可以在任何地方使用。

### 未宣告的變數其實是全域物件的屬性

之所以會有前述範例的結果, 是因為當 JavaScript 看到識別字時, 會一層層的往外找尋是否有符合該名稱的宣告, 例如：

```javascript
let a = 23

function foo() {
  console.log(a)
  b = 24;
  {
    c = 25;
    {
      console.log(b)
    }
  }
}

foo();
```

執行結果如下：

```
23
24
```

在 `foo` 函式中因為沒有宣告 `a`, 所以會往上一層找到全域變數 `a`；而最內層區塊列印的 `b` 也是往上一層區塊找到的 `b`。

如果在全域變數裡也找不到, 就會往[全域物件 `globalThis`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/globalThis) 找它的屬性, 這也是為什麼你可以直接以 `alert` 叫用定義在 `globalThis` 物件內的 `alert`：

```javascript
alert('call globalThis.alert');
globalThis.alert('property of globalThis');
```

以上兩種寫法其實是一樣的, 當 JavaScript 看到 `alert` 時, 會發現程式中並沒有定義 `alert` 函式, 因此會往全域物件 `globalThis` 尋找, 發現全域物件有 `alert` 屬性, 因此變成叫用 `globalThis.alert`。

在設定值的時候也是一樣, 對於沒有宣告過的識別字, JavaScript 會將之當成是要設定全域物件的屬性, 例如：

```javascript
a = 23;
globalThis.b = 24;
console.log(globalThis.a);
console.log(b);
```

執行結果如下：

```
23
24
```

第一行要設定 `a` 時, 會發現程式中沒有宣告過 `a`, 因此實際上執行的是 `globalThis.a = 23`, 你可以在第三行看到透過 `globalThis.a` 取用的就是同一份資料。相同的道理, 第二行雖然是設定 `globalThis` 的 `b` 屬性, 但是在第四行卻可以像是使用變數一樣直接以 `b` 來取得屬性值。 

你可以在任何地方用這種方式幫全域物件增加屬性, 並且以像是全域變數的方式使用該屬性。也就是說, 若不使用 `var`、`let`、`const` 宣告而直接設定值, 並不會建立變數, 而是**設定全域物件 `globalThis` 的屬性**。這樣的作法看起來好像很方便, 隨時想用就用, 但是卻容易造成混淆, 搞不清楚到底是在哪裡設定初值, 若要避免這個問題, 可以強制使用**嚴格模式**, 例如：

```javascript
'use strict'
a = 23;
console.log(a);
```

執行時就會引發錯誤：

```
Uncaught ReferenceError: assignment to undeclared variable a
```

它會認為你是設值給一個未宣告的變數。

### var 全域變數與純全域物件屬性的差異

你可能會想到, 前面不是有提到以 `var` 宣告的全域變數也會變成全域物件的屬性, 這樣和剛剛提到單純全域物件的屬性不是一樣嗎？還記得前面說明過, 以 `var` 宣告的全域變數會成為全域物件中不可設定的屬性, 具體的表現就是你無法用 `delete` 移除它, 但若是純全域物件的屬性, 就可以用 `delete` 移除, 例如：

```javascript
globalThis.a = 23
console.log(a)
delete a
console.log(a)
```

執行結果如下：

```
23
Uncaught ReferenceError: a is not defined
```

第二次要列印 `a` 時, 就會因為第三行已經將 `a` 移除變成未定義的識別字而引發錯誤。

這樣的差異很合理, 因為以 `var` 宣告的全域變數是真的全域變數, 如果可以刪除, 就不再是可以在程式中任何地方取用的全域變數了, 但是全域物件本來就是一個 JavaScript 物件, 自然可以隨意增刪屬性。

## 小結

以上我們就把宣告變數的幾種方法介紹完了, 希望有助於釐清為什麼這裡可以使用這個變數、或者是為什麼這個變數變成沒有定義的疑惑。簡單來說, 為了避免意外, 建議在程式中都只以 `let`、`const` 宣告, 不要使用 `var`, 也不要隨意幫全域物件新增屬性。