JavaScript 因為歷史悠久, 所以你可能會遇到經過多人更改過的 JavaScript 程式, 裡面混雜了不同時期 JavaScript 的語法, 導致有時候 JavaScript 程式就是難懂, 本文將針對宣告變數的幾種方法加以說明, 期望能讓大家快速理解其中的差別。
使用 var 宣告函式層級的變數
過去最常看到的變數宣告方是就是使用 var
, 它宣告的變數是函式層級, 也就是只要離開宣告時所在的函式, 這個變數就失效了, 例如:
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
宣告變數, 它就會變成全域變數, 在程式內任何地方都可以取用, 例如:
var a = 23;
function foo() {
console.log(a);
a = 24;
}
foo();
console.log(a);
執行結果如下:
23
24
宣告與設定初值分離
你也可以把宣告和設定初值分開來, 不一定要同時完成, 像是這樣:
var a;
a = 23;
console.log(a);
如果你希望在同一個地方集中宣告變數, 但是在要用到該變數的時候才設定值, 這樣的寫法就會很有用。
重複宣告
你也可以重複宣告同一個變數, 只要沒有設定新的值, 就會保留原值, 例如:
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)
, 會把宣告變數的動作提升到執行其他程式前先完成, 意思就是在進入變數的有效範圍內時, 會在執行第一行程式前就先宣告變數。因此, 在執行第一行程式的時候, 變數就已經存在了。例如:
console.log(a);
var a = 23;
console.log(a);
執行結果如下:
undefined
23
由於在執行第一行程式前, 就會先宣告變數, 因此第一行程式並不會引發變數尚未宣告的錯誤。不過對於以 var
宣告的變數, 變數提升只會先宣告變數, 並不會執行設定初值的程式, 以上例來說就是不會執行 a = 23
, 而是設定初值為 undefined
, 所以你會看到第一行印出 a
的值是 undefined
。等執行到第二行才會設定變數 a
的值為 23, 因此第三行就會印出 23 了。
全域變數會成為全域物件的屬性
以 var
宣告的全域變數會成為全域物件的屬性, 例如:
var a = 23;
console.log(globalThis.a);
執行結果如下:
23
不過這個屬性是不可設定 (non-configurable) 的, 也就是不能使用 delete
移除, 例如以下的程式雖然不會發生錯誤, 但是 delete
卻沒有作用:
var a = 23;
delete a;
delete globalThis.a;
console.log(a);
執行結果 a
仍然存在, 印出的值也是正確的:
23
如果採用嚴格模式, 就會看到錯誤訊息:
'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
宣告的變數只要出了所在的區塊, 就會失效, 例如:
{
let a = 23;
console.log(a);
}
console.log(a);
執行結果如下:
23
Uncaught ReferenceError: a is not defined
由於第二次取用 a
時已經離開了宣告變數的區塊, 因此變數 a
已經失效, 就會引發未定義識別字的錯誤。
分辨區塊
在大部分的情況下, 我們很容易辨識區塊, 不過在像是 for
的敘述中, 初始設定也是區塊的一部份, 因此在初始設定內宣告的變數在 for
結束後也一樣會失效, 例如:
for(let i = 0;i < 2;i++)
{
console.log(i);
}
console.log(i);
執行結果如下:
0
1
Uncaught ReferenceError: i is not defined
如果改用 var
宣告變數, 由於並沒有離開函式範圍, 所以不會引發錯誤, 例如:
for(var i = 0;i < 2;i++)
{
console.log(i);
}
console.log(i);
執行結果最後會印出迴圈結束時的 i
值:
0
1
2
全域變數
在任何區塊外使用 let
建立的變數一樣是全域變數, 可在程式中任何地方取用, 例如:
let i = 0;
for(i = 0;i < 2;i++) {}
console.log(i);
結果如下:
2
不能重複宣告變數
使用 let
宣告的變數不能重複宣告, 例如:
let a = 23;
console.log(a);
let a;
執行時會直接引發錯誤告訴你 a
已經宣告過了:
SyntaxError: Identifier 'a' has already been declared
變數提升不會設定初值
以 let
宣告變數也一樣具有變數提升功能, 但是並不會設定初值, 在使用變數前一定要先透過 let
宣告, 例如:
console.log(a);
let a = 0;
執行時就會引發錯誤:
Uncaught ReferenceError: can't access lexical declaration 'a' before initialization
表示不能在設定初值前就取用已宣告的變數 a
。
以 let 宣告的變數不會成為全域物件的屬性
以 let
宣告的變數並不會像是以 var
宣告的變數那樣成為全域物件的屬性, 例如:
let a = 23;
console.log(globalThis.a);
執行時印出的並不是 a
的值, 而是 undefined
:
undefined
表示全域物件中並沒有 a
這個屬性。
使用 const 宣告不能變更的變數
你也可以使用 const
宣告變數, 不過這種變數如同 const
字面所示, 是不能變的, 中文翻譯為常數。先來看看以下的例子:
const a = 23;
console.log(a);
a = 24;
執行後如下:
23
Uncaught TypeError: invalid assignment to const 'a'
在第三行嘗試設定常數內容時就會引發錯誤, 告訴你不能設定以 const
宣告的常數。
宣告常數時一定要設定值
以 const
宣告常數時必須一併設定初值, 不能將宣告與設定初值分開進行, 像是以下的例子就會引發錯誤:
const a;
a = 23;
console.log(a);
執行結果如下:
Uncaught SyntaxError: missing = in const declaration
錯誤訊息告訴我們在 const
宣告時少了設定初值的 =
。
變更常數所參照的物件
請特別注意, 不能變更以 const
宣告的常數指的是不能重新設定常數本身, 如果常數的內容是一個物件, 你還是可以變更物件內的屬性, 例如:
const a = {name: "John"};
a.name = "Mary";
console.log(a);
執行結果如下:
Object { name: "Mary" }
由於變更的是物件的內容, 而不是變更常數 a
, 所以可以成功執行。同樣的道理, 如果常數的內容是一個陣列, 也可以變更陣列內的項目:
const a = [1, 2, 3];
a.push(4);
a[0] = 10;
console.log(a);
執行結果如下:
Array(4) [ 10, 2, 3, 4 ]
除了不能重新設值外, const
跟 let
是一樣的。
沒有宣告直接設定變數
JavaScript 是很寬鬆的, 你甚至會看到有些程式中根本沒有宣告就直接設定變數的值, 像是這樣:
a = 23;
console.log(a);
執行時並不會引發錯誤, 而且可以正確印出 a
的值:
23
你甚至還可以隨意在函式或是區塊直接用同樣的方式運作:
function foo() {
a = 23
}
{
b = 24
}
foo()
console.log(a);
console.log(b);
執行結果如下:
23
24
你會看到 a
和 b
雖然是在函式以及區塊內設定, 可是不像是 var
或是 let
有範圍的限制, 兩個都變成全域變數那樣可以在任何地方使用。
未宣告的變數其實是全域物件的屬性
之所以會有前述範例的結果, 是因為當 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
找它的屬性, 這也是為什麼你可以直接以 alert
叫用定義在 globalThis
物件內的 alert
:
alert('call globalThis.alert');
globalThis.alert('property of globalThis');
以上兩種寫法其實是一樣的, 當 JavaScript 看到 alert
時, 會發現程式中並沒有定義 alert
函式, 因此會往全域物件 globalThis
尋找, 發現全域物件有 alert
屬性, 因此變成叫用 globalThis.alert
。
在設定值的時候也是一樣, 對於沒有宣告過的識別字, 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
的屬性。這樣的作法看起來好像很方便, 隨時想用就用, 但是卻容易造成混淆, 搞不清楚到底是在哪裡設定初值, 若要避免這個問題, 可以強制使用嚴格模式, 例如:
'use strict'
a = 23;
console.log(a);
執行時就會引發錯誤:
Uncaught ReferenceError: assignment to undeclared variable a
它會認為你是設值給一個未宣告的變數。
var 全域變數與純全域物件屬性的差異
你可能會想到, 前面不是有提到以 var
宣告的全域變數也會變成全域物件的屬性, 這樣和剛剛提到單純全域物件的屬性不是一樣嗎?還記得前面說明過, 以 var
宣告的全域變數會成為全域物件中不可設定的屬性, 具體的表現就是你無法用 delete
移除它, 但若是純全域物件的屬性, 就可以用 delete
移除, 例如:
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
, 也不要隨意幫全域物件新增屬性。
Top comments (0)