let
const
let
和 const
進行區塊範圍
const
建立不可變的變數
const
沒有讓值不可變const
var
變數的生命週期let
變數的生命週期typeof
會對暫時性死區中的變數擲出 ReferenceError
let
和 const
for
迴圈for-of
迴圈和 for-in
迴圈const
相較於 let
相較於 var
ES6 提供了兩種新的宣告變數方法:let
和 const
,它們大多取代了 ES5 宣告變數的方法 var
。
let
let
的運作方式類似於 var
,但它宣告的變數是區塊範圍的,它只存在於目前的區塊中。var
是函式範圍的。
在以下程式碼中,你可以看到宣告 let
的變數 tmp
只存在於從 A 行開始的區塊中
function
order
(
x
,
y
)
{
if
(
x
>
y
)
{
// (A)
let
tmp
=
x
;
x
=
y
;
y
=
tmp
;
}
console
.
log
(
tmp
===
x
);
// ReferenceError: tmp is not defined
return
[
x
,
y
];
}
const
const
的運作方式類似於 let
,但您宣告的變數必須立即初始化,而且其值之後無法變更。
const
foo
;
// SyntaxError: missing = in const declaration
const
bar
=
123
;
bar
=
456
;
// TypeError: `bar` is read-only
由於 for-of
會為每個迴圈迭代建立一個 繫結(變數的儲存空間),因此對迴圈變數宣告 const
是可以的
for
(
const
x
of
[
'a'
,
'b'
])
{
console
.
log
(
x
);
}
// Output:
// a
// b
下表概述了在 ES6 中宣告變數的六種方式(靈感來自 kangax 的表格)
提升 | 範圍 | 建立全域屬性 | |
---|---|---|---|
var |
宣告 | 函式 | 是 |
let |
暫時性死區 | 區塊 | 否 |
const |
暫時性死區 | 區塊 | 否 |
函式 |
完整 | 區塊 | 是 |
class |
否 | 區塊 | 否 |
import |
完整 | 模組全域 | 否 |
let
和 const
進行區塊範圍設定 let
和 const
都會建立 區塊範圍 的變數,它們只存在於它們周圍最內層的區塊中。下列程式碼示範了 const
宣告的變數 tmp
只存在於 if
陳述式的區塊中
function
func
()
{
if
(
true
)
{
const
tmp
=
123
;
}
console
.
log
(
tmp
);
// ReferenceError: tmp is not defined
}
相反地,var
宣告的變數是函式範圍的
function
func
()
{
if
(
true
)
{
var
tmp
=
123
;
}
console
.
log
(
tmp
);
// 123
}
區塊範圍設定表示您可以在函式中隱藏變數
function
func
()
{
const
foo
=
5
;
if
(
···
)
{
const
foo
=
10
;
// shadows outer `foo`
console
.
log
(
foo
);
// 10
}
console
.
log
(
foo
);
// 5
}
const
建立不可變的變數 由 let
建立的變數是可變的
let
foo
=
'abc'
;
foo
=
'def'
;
console
.
log
(
foo
);
// def
常數(由 const
建立的變數)是不可變的,您無法為它們指定不同的值
const
foo
=
'abc'
;
foo
=
'def'
;
// TypeError
const
沒有讓值變成不可變的 const
只表示變數永遠具有相同的值,但並不表示值本身是或會變成不可變的。例如,obj
是常數,但它指向的值是可變的,我們可以為它新增屬性
const
obj
=
{};
obj
.
prop
=
123
;
console
.
log
(
obj
.
prop
);
// 123
不過,我們無法為 obj
指定不同的值
obj
=
{};
// TypeError
如果您希望 obj
的值不可變,您必須自行處理。例如,透過 凍結它
const
obj
=
Object
.
freeze
({});
obj
.
prop
=
123
;
// TypeError
Object.freeze()
是淺層的 請記住,Object.freeze()
是淺層的,它僅凍結其參數的屬性,而非儲存在其屬性中的物件。例如,物件 obj
已凍結
>
const
obj
=
Object
.
freeze
(
{
foo
:
{
}
}
);
>
obj
.
bar
=
123
TypeError
:
Can
't add property bar, object is not extensible
> obj.foo = {}
TypeError: Cannot assign to read only property '
foo
'
of
#
<
Object
>
但物件 obj.foo
則沒有。
> obj.foo.qux = 'abc';
> obj.foo.qux
'abc'
const
一旦建立 const
變數,就無法變更。但這並不表示您無法重新進入其範圍並從頭開始,使用新的值。例如,透過迴圈
function
logArgs
(...
args
)
{
for
(
const
[
index
,
elem
]
of
args
.
entries
())
{
// (A)
const
message
=
index
+
'. '
+
elem
;
// (B)
console
.
log
(
message
);
}
}
logArgs
(
'Hello'
,
'everyone'
);
// Output:
// 0. Hello
// 1. everyone
此程式碼中有兩個 const
宣告,在 A 行和 B 行。在每次迴圈反覆運算期間,其常數都有不同的值。
由 let
或 const
宣告的變數具有所謂的暫時性死區 (TDZ):進入其範圍時,在執行到達宣告之前,無法存取 (取得或設定) 該變數。讓我們比較由 var
宣告的變數 (沒有 TDZ) 和由 let
宣告的變數 (有 TDZ) 的生命週期。
var
宣告的變數的生命週期 var
變數沒有暫時性死區。其生命週期包含下列步驟
var
變數的範圍 (其周圍函式) 時,會為其建立儲存空間 (一個繫結)。變數會立即初始化,將其設定為 undefined
。undefined
。let
宣告的變數的生命週期 透過 let
宣告的變數有暫時性死區,其生命週期如下
let
變數的範圍 (其周圍區塊) 時,會為其建立儲存空間 (一個繫結)。變數仍未初始化。ReferenceError
。undefined
。const
變數的運作方式類似於 let
變數,但它們必須具有初始化器 (亦即,立即設定為一個值),且無法變更。
在 TDZ 內,如果變數被取得或設定,將會拋出例外
let
tmp
=
true
;
if
(
true
)
{
// enter new scope, TDZ starts
// Uninitialized binding for `tmp` is created
console
.
log
(
tmp
);
// ReferenceError
let
tmp
;
// TDZ ends, `tmp` is initialized with `undefined`
console
.
log
(
tmp
);
// undefined
tmp
=
123
;
console
.
log
(
tmp
);
// 123
}
console
.
log
(
tmp
);
// true
如果有一個初始化器,則 TDZ 會在初始化器評估後且結果已指派給變數之後結束
let
foo
=
console
.
log
(
foo
);
// ReferenceError
以下程式碼示範了死區實際上是時間性的(基於時間),而非空間性的(基於位置)
if
(
true
)
{
// enter new scope, TDZ starts
const
func
=
function
()
{
console
.
log
(
myVar
);
// OK!
};
// Here we are within the TDZ and
// accessing `myVar` would cause a `ReferenceError`
let
myVar
=
3
;
// TDZ ends
func
();
// called outside TDZ
}
typeof
會為 TDZ 中的變數拋出 ReferenceError
如果您透過 typeof
存取時間死區中的變數,您會得到一個例外
if
(
true
)
{
console
.
log
(
typeof
foo
);
// ReferenceError (TDZ)
console
.
log
(
typeof
aVariableThatDoesntExist
);
// 'undefined'
let
foo
;
}
為什麼?其原理如下:foo
並未未宣告,而是未初始化。您應該知道它的存在,但您並不知道。因此,收到警告似乎是可取的。
此外,這種檢查僅對有條件建立全域變數有用。這是在一般程式中不需要執行的動作。
當涉及到有條件建立變數時,您有兩個選項。
選項 1 – typeof
和 var
if
(
typeof
someGlobal
===
'undefined'
)
{
var
someGlobal
=
{
···
};
}
此選項僅在全域範圍內有效(因此不在 ES6 模組內)。
選項 2 – window
if
(
!
(
'someGlobal'
in
window
))
{
window
.
someGlobal
=
{
···
};
}
有幾個原因讓 const
和 let
具有時間死區
const
:讓 const
正確運作很困難。引用 Allen Wirfs-Brock:「TDZ…為 const
提供合理的語意。對於該主題有大量的技術討論,而 TDZ 成為最佳解決方案。」let
也有暫時性死區,因此在 let
和 const
之間切換不會以意外的方式改變行為。undefined
,則該值可能會與其防護措施提供的保證相衝突。本節的來源
let
和 const
下列迴圈允許你在標頭中宣告變數
for
for-in
for-of
若要進行宣告,你可以使用 var
、let
或 const
。它們各自有不同的效果,我將在下面說明。
for
迴圈 在 for
迴圈標頭中使用 var
宣告變數會為該變數建立單一繫結(儲存空間)
const
arr
=
[];
for
(
var
i
=
0
;
i
<
3
;
i
++
)
{
arr
.
push
(()
=>
i
);
}
arr
.
map
(
x
=>
x
());
// [3,3,3]
三個箭頭函式的本體中的每個 i
都參照相同的繫結,這就是為什麼它們都傳回相同的值。
如果你使用 let
宣告變數,則會為每個迴圈反覆建立新的繫結
const
arr
=
[];
for
(
let
i
=
0
;
i
<
3
;
i
++
)
{
arr
.
push
(()
=>
i
);
}
arr
.
map
(
x
=>
x
());
// [0,1,2]
這一次,每個 i
都參照特定反覆的繫結,並保留當時的目前值。因此,每個箭頭函式都傳回不同的值。
const
的運作方式與 var
相同,但你無法變更使用 const
宣告變數的初始值
// TypeError: Assignment to constant variable
// (due to i++)
for (const i=0; i<3; i++) {
console.log(i);
}
一開始,為每個反覆取得新的繫結可能看起來很奇怪,但當你使用迴圈建立參照迴圈變數的函式時,它非常有用,如 後面的章節 所述。
for-of
迴圈和 for-in
迴圈 在 for-of
迴圈中,var
會建立單一繫結
const
arr
=
[];
for
(
var
i
of
[
0
,
1
,
2
])
{
arr
.
push
(()
=>
i
);
}
arr
.
map
(
x
=>
x
());
// [2,2,2]
const
會為每次反覆運算建立一個不可變繫結
const
arr
=
[];
for
(
const
i
of
[
0
,
1
,
2
])
{
arr
.
push
(()
=>
i
);
}
arr
.
map
(
x
=>
x
());
// [0,1,2]
let
也會為每次反覆運算建立一個繫結,但它建立的繫結是可變的。
for-in
迴圈的運作方式與 for-of
迴圈類似。
以下是一個顯示三個連結的 HTML 頁面
<!
doctype
html
>
<
html
>
<
head
>
<
meta
charset
=
"UTF-8"
>
<
/head>
<
body
>
<
div
id
=
"content"
><
/div>
<
script
>
const
entries
=
[
[
'yes'
,
'ja'
],
[
'no'
,
'nein'
],
[
'perhaps'
,
'vielleicht'
],
];
const
content
=
document
.
getElementById
(
'content'
);
for
(
const
[
source
,
target
]
of
entries
)
{
// (A)
content
.
insertAdjacentHTML
(
'beforeend'
,
`<div><a id="
${
source
}
" href="">
${
source
}
</a></div>`
);
document
.
getElementById
(
source
).
addEventListener
(
'click'
,
(
event
)
=>
{
event
.
preventDefault
();
alert
(
target
);
// (B)
});
}
<
/script>
<
/body>
<
/html>
顯示的內容取決於變數 target
(B 行)。如果我們在 A 行中使用 var
而非 const
,整個迴圈只會有一個繫結,而 target
之後的值會是 'vielleicht'
。因此,不論您按一下哪個連結,您都會看到翻譯「vielleicht」。
很幸運地,使用 const
後,我們會為每次迴圈反覆運算取得一個繫結,而翻譯也會正確顯示。
如果您使用 let
宣告一個與參數同名的變數,您會得到一個靜態(載入時間)錯誤
function
func
(
arg
)
{
let
arg
;
// static error: duplicate declaration of `arg`
}
在區塊中執行相同的動作會隱藏參數
function
func
(
arg
)
{
{
let
arg
;
// shadows parameter `arg`
}
}
相反地,使用 var
宣告一個與參數同名的變數不會有任何動作,就像在同一個範圍內重新宣告一個 var
變數不會有任何動作一樣。
function
func
(
arg
)
{
var
arg
;
// does nothing
}
function
func
(
arg
)
{
{
// We are still in same `var` scope as `arg`
var
arg
;
// does nothing
}
}
如果參數有預設值,它們會被視為一系列的 let
陳述式,並且會受到暫時性死區的影響
// OK: `y` accesses `x` after it has been declared
function
foo
(
x
=
1
,
y
=
x
)
{
return
[
x
,
y
];
}
foo
();
// [1,1]
// Exception: `x` tries to access `y` within TDZ
function
bar
(
x
=
y
,
y
=
2
)
{
return
[
x
,
y
];
}
bar
();
// ReferenceError
參數預設值的範圍與主體的範圍是分開的(前者包圍後者)。這表示在參數預設值「內部」定義的方法或函式不會看到主體的區域變數
const
foo
=
'outer'
;
function
bar
(
func
=
x
=>
foo
)
{
const
foo
=
'inner'
;
console
.
log
(
func
());
// outer
}
bar
();
JavaScript 的 全域物件(在網頁瀏覽器中為 window
,在 Node.js 中為 global
)比較像是一個錯誤,而不是一個功能,特別是在效能方面。這就是為什麼 ES6 引入一個區別是有道理的
var
宣告let
宣告const
宣告請注意,模組的主體不會在全域範圍內執行,只有腳本會。因此,各種變數的環境會形成下列鏈。
函式宣告…
let
一樣。var
一樣。下列程式碼示範函式宣告的提升
{
// Enter a new scope
console
.
log
(
foo
());
// OK, due to hoisting
function
foo
()
{
return
'hello'
;
}
}
類別宣告…
類別不會提升可能會令人驚訝,因為在底層,它們會建立函式。這種行為的理由是,它們的 extends
子句的值是透過運算式定義的,而這些運算式必須在適當的時間執行。
{
// Enter a new scope
const
identity
=
x
=>
x
;
// Here we are in the temporal dead zone of `MyClass`
const
inst
=
new
MyClass
();
// ReferenceError
// Note the expression in the `extends` clause
class
MyClass
extends
identity
(
Object
)
{
}
}
const
與 let
與 var
我建議總是使用 let
或 const
const
。只要變數不變更其值,你就可以使用它。換句話說:變數不應該是指定運算的左側或 ++
或 --
的運算元。變更 const
變數所參考的物件是允許的
const
foo
=
{};
foo
.
prop
=
123
;
// OK
你甚至可以在 for-of
迴圈中使用 const
,因為每個迴圈迭代都會建立一個(不可變)繫結。
for
(
const
x
of
[
'a'
,
'b'
])
{
console
.
log
(
x
);
}
// Output:
// a
// b
在 for-of
迴圈的主體內,x
無法變更。
let
– 當變數的初始值稍後變更時。
let
counter
=
0
;
// initial value
counter
++
;
// change
let
obj
=
{};
// initial value
obj
=
{
foo
:
123
};
// change
var
。如果你遵循這些規則,var
將只會出現在舊版程式碼中,表示需要小心重構。
var
做了一件事是 let
和 const
沒有做的:透過它宣告的變數會變成全域物件的屬性。然而,這通常不是一件好事。你可以透過指定給 window
(在瀏覽器中)或 global
(在 Node.js 中)來達成相同的效果。
另一種方法是僅對完全不可變的事物(原始值和凍結物件)使用 const
。然後我們有兩種方法
const
: const
標記不可變繫結。let
: const
標記不可變的值。我稍微偏好 #1,但 #2 也可以。