DEV Community

codemee
codemee

Posted on • Edited on

JavaScript 的 var、let 與 const

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

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

過去最常看到的變數宣告方是就是使用 var, 它宣告的變數是函式層級, 也就是只要離開宣告時所在的函式, 這個變數就失效了, 例如:

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

foo();
console.log(a);
Enter fullscreen mode Exit fullscreen mode

執行結果如下:

23
Uncaught ReferenceError: a is not defined
Enter fullscreen mode Exit fullscreen mode

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

全域變數

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

var a = 23;

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

foo();
console.log(a);
Enter fullscreen mode Exit fullscreen mode

執行結果如下:

23
24
Enter fullscreen mode Exit fullscreen mode

宣告與設定初值分離

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

var a;
a = 23;
console.log(a);
Enter fullscreen mode Exit fullscreen mode

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

重複宣告

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

var a = 23;
console.log(a);
var a;
console.log(a);
var a = 24;
console.log(a);
Enter fullscreen mode Exit fullscreen mode

執行結果如下:

23
23
24
Enter fullscreen mode Exit fullscreen mode

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

變數提升 (variable hoisting)

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

console.log(a);
var a = 23;
console.log(a);
Enter fullscreen mode Exit fullscreen mode

執行結果如下:

undefined
23
Enter fullscreen mode Exit fullscreen mode

由於在執行第一行程式前, 就會先宣告變數, 因此第一行程式並不會引發變數尚未宣告的錯誤。不過對於以 var 宣告的變數, 變數提升只會先宣告變數, 並不會執行設定初值的程式, 以上例來說就是不會執行 a = 23, 而是設定初值為 undefined, 所以你會看到第一行印出 a 的值是 undefined。等執行到第二行才會設定變數 a 的值為 23, 因此第三行就會印出 23 了。

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

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

var a = 23;
console.log(globalThis.a);
Enter fullscreen mode Exit fullscreen mode

執行結果如下:

23
Enter fullscreen mode Exit fullscreen mode

不過這個屬性是不可設定 (non-configurable) 的, 也就是不能使用 delete 移除, 例如以下的程式雖然不會發生錯誤, 但是 delete 卻沒有作用:

var a = 23;
delete a;
delete globalThis.a;
console.log(a);
Enter fullscreen mode Exit fullscreen mode

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

23
Enter fullscreen mode Exit fullscreen mode

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

'use strict';

var a = 23;
delete globalThis.a;
console.log(a);
Enter fullscreen mode Exit fullscreen mode

執行結果如下:

Uncaught TypeError: property "a" is non-configurable and can't be deleted
Enter fullscreen mode Exit fullscreen mode

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

所謂的區塊, 就是由一對大括號括起來的區域, 使用 let 宣告的變數只要出了所在的區塊, 就會失效, 例如:

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

console.log(a);
Enter fullscreen mode Exit fullscreen mode

執行結果如下:

23
Uncaught ReferenceError: a is not defined
Enter fullscreen mode Exit fullscreen mode

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

分辨區塊

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

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

console.log(i);
Enter fullscreen mode Exit fullscreen mode

執行結果如下:

0
1
Uncaught ReferenceError: i is not defined
Enter fullscreen mode Exit fullscreen mode

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

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

console.log(i);
Enter fullscreen mode Exit fullscreen mode

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

0
1
2
Enter fullscreen mode Exit fullscreen mode

全域變數

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

let i = 0;

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

console.log(i);
Enter fullscreen mode Exit fullscreen mode

結果如下:

2
Enter fullscreen mode Exit fullscreen mode

不能重複宣告變數

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

let a = 23;
console.log(a);
let a;
Enter fullscreen mode Exit fullscreen mode

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

SyntaxError: Identifier 'a' has already been declared
Enter fullscreen mode Exit fullscreen mode

變數提升不會設定初值

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

console.log(a);
let a = 0;
Enter fullscreen mode Exit fullscreen mode

執行時就會引發錯誤:

Uncaught ReferenceError: can't access lexical declaration 'a' before initialization
Enter fullscreen mode Exit fullscreen mode

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

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

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

let a = 23;
console.log(globalThis.a);
Enter fullscreen mode Exit fullscreen mode

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

undefined
Enter fullscreen mode Exit fullscreen mode

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

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

你也可以使用 const 宣告變數, 不過這種變數如同 const 字面所示, 是不能變的, 中文翻譯為常數。先來看看以下的例子:

const a = 23;
console.log(a);
a = 24;
Enter fullscreen mode Exit fullscreen mode

執行後如下:

23
Uncaught TypeError: invalid assignment to const 'a'
Enter fullscreen mode Exit fullscreen mode

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

宣告常數時一定要設定值

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

const a;
a = 23;
console.log(a);
Enter fullscreen mode Exit fullscreen mode

執行結果如下:

Uncaught SyntaxError: missing = in const declaration
Enter fullscreen mode Exit fullscreen mode

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

變更常數所參照的物件

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

const a = {name: "John"};
a.name = "Mary";
console.log(a);
Enter fullscreen mode Exit fullscreen mode

執行結果如下:

Object { name: "Mary" }
Enter fullscreen mode Exit fullscreen mode

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

const a = [1, 2, 3];
a.push(4);
a[0] = 10;
console.log(a);
Enter fullscreen mode Exit fullscreen mode

執行結果如下:

Array(4) [ 10, 2, 3, 4 ]
Enter fullscreen mode Exit fullscreen mode

除了不能重新設值外, constlet 是一樣的。

沒有宣告直接設定變數

JavaScript 是很寬鬆的, 你甚至會看到有些程式中根本沒有宣告就直接設定變數的值, 像是這樣:

a = 23;
console.log(a);
Enter fullscreen mode Exit fullscreen mode

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

23
Enter fullscreen mode Exit fullscreen mode

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

function foo() {
  a = 23
}

{
  b = 24
}

foo()
console.log(a);
console.log(b);
Enter fullscreen mode Exit fullscreen mode

執行結果如下:

23
24
Enter fullscreen mode Exit fullscreen mode

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

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

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

let a = 23

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

foo();
Enter fullscreen mode Exit fullscreen mode

執行結果如下:

23
24
Enter fullscreen mode Exit fullscreen mode

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

如果在全域變數裡也找不到, 就會往全域物件 globalThis 找它的屬性, 這也是為什麼你可以直接以 alert 叫用定義在 globalThis 物件內的 alert

alert('call globalThis.alert');
globalThis.alert('property of globalThis');
Enter fullscreen mode Exit fullscreen mode

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

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

a = 23;
globalThis.b = 24;
console.log(globalThis.a);
console.log(b);
Enter fullscreen mode Exit fullscreen mode

執行結果如下:

23
24
Enter fullscreen mode Exit fullscreen mode

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

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

'use strict'
a = 23;
console.log(a);
Enter fullscreen mode Exit fullscreen mode

執行時就會引發錯誤:

Uncaught ReferenceError: assignment to undeclared variable a
Enter fullscreen mode Exit fullscreen mode

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

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

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

globalThis.a = 23
console.log(a)
delete a
console.log(a)
Enter fullscreen mode Exit fullscreen mode

執行結果如下:

23
Uncaught ReferenceError: a is not defined
Enter fullscreen mode Exit fullscreen mode

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

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

小結

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

Top comments (0)