DEV Community

codemee
codemee

Posted on • Edited on

4 2

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, 也不要隨意幫全域物件新增屬性。

Tiugo image

Modular, Fast, and Built for Developers

CKEditor 5 gives you full control over your editing experience. A modular architecture means you get high performance, fewer re-renders and a setup that scales with your needs.

Start now

Top comments (0)

Neon image

Next.js applications: Set up a Neon project in seconds

If you're starting a new project, Neon has got your databases covered. No credit cards. No trials. No getting in your way.

Get started →

👋 Kindness is contagious

Engage with a wealth of insights in this thoughtful article, valued within the supportive DEV Community. Coders of every background are welcome to join in and add to our collective wisdom.

A sincere "thank you" often brightens someone’s day. Share your gratitude in the comments below!

On DEV, the act of sharing knowledge eases our journey and fortifies our community ties. Found value in this? A quick thank you to the author can make a significant impact.

Okay