ES6 提供了兩種宣告變數的新方法: let
和 const
,它們主要取代 ES5 宣告變數的方式:var
。
9.1.1 let
let
與 var
類似,但它宣告的變數是具有塊級作用域的(block-scoped),它只存在於當前塊中。 var
是函式作用域(function-scoped)。
在下面的程式碼中,您可以看到 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]
}
複製程式碼
9.1.2 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
複製程式碼
9.1.3 宣告變數的方式
下表概述了在 ES6 中宣告變數的六種方式(受 kangax 表的啟發 ):
提升形式 | 作用域形式 | 建立全域性屬性 | |
---|---|---|---|
var |
Declaration | Function | Yes |
let |
Temporal dead zone | Block | No |
const |
Temporal dead zone | Block | No |
function |
Complete | Block | Yes |
classs |
No | Block | No |
import |
Complete | Module-global | No |
9.2 通過 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
}
複製程式碼
9.3 const
建立不可變變數
let
建立的變數是可變的:
let foo = 'abc';
foo = 'def';
console.log(foo); // def
複製程式碼
常量(const
建立的變數)是不可變的,不能再給它們賦不同的值:
const foo = 'abc';
foo = 'def'; // TypeError
複製程式碼
規範細節:更改 const
變數總是丟擲 TypeError
通常,根據 SetMutableBinding() ,更改不可變繫結僅在嚴格模式下導致異常。 但 const
宣告的變數總是產生嚴格的繫結 - 參見 FunctionDeclarationInstantiation(func, argumentsList) 。
9.3.1 陷阱: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
複製程式碼
9.3.1.1 陷阱: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'
複製程式碼
9.3.2 迴圈體中的 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) 中。在每次迴圈迭代期間,它們的常量有不同的值。
9.4 臨時死區(temporal dead zone)
由 let
或const
宣告的變數具有所謂的 臨時死區(TDZ):當進入其作用域時,在執行到達宣告之前不能訪問(獲取或設定)它。讓我們比較 var
變數(沒有TDZ)和let
變數(有TDZ)的生命週期。
9.4.1 var
變數的生命週期
var變數沒有臨時死區。 他們的生命週期包括以下步驟:
- 當進入
var
變數的作用域(其所在的函式)時,將為它建立儲存空間(繫結)。 通過將變數設定為undefined
,立即初始化變數。 - 當作用域內的執行到達宣告時,該變數被設定為初始化 程式指定的值(賦值)- 如果有的話。如果沒有,則變數的值仍未
undefined
。
9.4.2 let
變數的生命週期
通過 let
宣告的變數有臨時死區,它們的生命週期如下所示:
- 當進入
let
變數的作用域(其所在的塊)時,將為其建立儲存空間(繫結)。變數仍然未初始化。 - 獲取或設定未初始化的變數會導致
ReferenceError
。 - 當範圍內的執行到達宣告時,該變數被設定為初始化程式指定的值(賦值)- 如果有的話。如果沒有,則將變數的值設定為
undefined
。
const
變數與 let
變數的工作方式類似,但它們必須具有初始化程式(即立即設定值)並且不能更改。
9.4.3 示例
在 TDZ 中,如果獲取或設定變數,則丟擲異常:
let tmp = true;
if (true) { // 這裡進入塊級空間,TDZ 開始
// 未被初始化的`tmp`儲存空間 被建立
console.log(tmp); // 異常:ReferenceError
let tmp; // TDZ 結束, `tmp` 被賦值為‘undefined’
console.log(tmp); //列印:undefined
tmp = 123;
console.log(tmp); // 列印:123
}
console.log(tmp); // 列印:true
複製程式碼
如果有初始化,則TDZ 會在初始化後並將結果賦值給變數後結束:
let foo = console.log(foo); // 列印:ReferenceError
複製程式碼
以下程式碼演示死區實際上是臨時的 (基於時間)而不是空間(基於位置):
if (true) { // 進入塊級空間,TDZ 開始
const func = function () {
console.log(myVar); // 這裡沒有問題
};
// 當前處於 TDZ 內部
// 如果 訪問 `myVar` 會出現異常 `ReferenceError`
let myVar = 3; // TDZ 結束
func(); // 此時 TDZ 不存在,呼叫func
}
複製程式碼
9.4.4 typeof
會為TDZ 中的變數丟擲 ReferenceError
如果通過 typeof
訪問時間死區中的變數,則會出現異常:
if (true) {
console.log(typeof foo); // ReferenceError (TDZ)
console.log(typeof aVariableThatDoesntExist); // 'undefined'
let foo;
}
複製程式碼
為什麼? 理由如下:foo
不是未宣告的,它是未初始化的。你應該意識到它的存在,但你沒有。因此,被警告似乎是可取的。
此外,這種檢查僅對有條件地建立全域性變數有用。在正常程式中不需要做的。
9.4.4.1 有條件地建立變數
在有條件地建立變數時,您有兩種選擇。
選項1 - typeof
和 var
:
if (typeof someGlobal === 'undefined') {
var someGlobal = { ··· };
}
複製程式碼
此選項僅適用於全域性範圍(因此不在ES6模組內)。
選項2 - window
:
if (!('someGlobal' in window)) {
window.someGlobal = { ··· };
}
複製程式碼
9.4.5 為什麼會出現臨時死區(TDZ)?
const
和 let
產生TDZ,有幾個原因:
- 捕獲程式設計錯誤:能夠在宣告之前訪問變數很奇怪。如果你這樣做了,可能是意外,你就應該得到警告。
- 對於
const
:使const
正常工作很困難。引用Allen Wirfs-Brock:“TDZs ......為const提供了理性的語義。 對該主題進行了重要的技術討論,並且TDZ 成為最佳解決方案。“let
也有一個臨時死區,這樣let
和const
之間的切換不會以意料之外的方式改變行為。 - 面向未來的防護:JavaScript最終可能會有一些防護,一種在執行時強制執行變數具有正確值的機制(想想執行時型別檢查)。如果變數的值在宣告之前
undefined
,那麼該值可能與 其保護所給出的保證 相沖突。
9.4.6 進一步閱讀
本節的來源:
9.5 迴圈頭部中的 let
和 const
以下迴圈允許您在其頭部宣告變數:
for
for-in
for-of
要進行宣告,可以使用var
, let
或 const
。 他們每個都有不同的影響,我將在下面解釋。
9.5.1 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
迴圈:規範中的每個迭代繫結
for
迴圈的求值 將var
作為第二種情況處理,並將 let/const
作為第三種情況。 只有 let
變數被新增到列表 perIterationLets
(步驟9)中,它作為倒數第二個引數 perIterationBindings
傳遞給ForBodyEvaluation()
。
9.5.2 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
迴圈的工作方式類似。
for-of
迴圈:規範中的每次迭代繫結
for-of
中的每次迭代繫結由 ForIn/OfBodyEvaluation處理。在步驟5.b中,建立一個新環境,並通過BindingInstantiation為其新增繫結(對於let
可變的,對於const
不可變的)。當前迭代值儲存在變數nextValue
,用於以兩種方式之一初始化繫結:
單變數宣告(步驟5.hi):通過 InitializeReferencedBinding
處理
解構(步驟5.i.iii):通過一個 BindingInitialization 情況 ( ForDeclaration )來處理,它呼叫另一個BindingInitialization ( BindingPattern )的情況。
9.5.3 為什麼每次迭代繫結是有用的?
以下是顯示三個連結的HTML頁面:
- 如果單擊“yes”,則將其翻譯為“ja”。
- 如果單擊“no”,則將其翻譯為“nein”。
- 如果單擊“perhaps”,則會將其翻譯為“vielleicht”。
<!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
,我們每個迴圈迭代都有一個繫結,並正確地顯示轉換。
9.6 引數作為變數
9.6.1 引數與區域性變數
如果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
}
}
複製程式碼
9.6.2 引數預設值和 臨時死區
如果引數具有預設值,則它們將被視為一系列 let
語句,並受臨時死區的影響:
// OK: `y` accesses `x` after it has been declared
function foo(x=1, y=x) {
return [x, y];
}
foo(); // [1,1]
// 異常: `x` 試圖 在TDZ 中訪問 `y`
function bar(x=y, y=2) {
return [x, y];
}
bar(); // ReferenceError
複製程式碼
9.6.3 引數預設值看不到主體的作用域
引數預設值的範圍與主體作用域分開(前者圍繞後者)。這意味著在引數預設值中定義的方法或函式不會看到主體的區域性變數
const foo = 'outer';
function bar(func = x => foo) {
const foo = 'inner'; // 不會被看到哦~
console.log(func()); // outer
}
bar();
複製程式碼
9.7 全域性物件
JavaScript的全域性物件 (Web瀏覽器中的 window
,Node.js中的 global
)更像是一個bug而不是一個功能,特別是在效能方面。這就是ES6 引入區別的原因:
- 全域性物件的所有屬性都是全域性變數。在全域性範圍中,以下宣告建立了這樣的屬性:
var
宣告- 函式宣告
- 但是現在還有全域性變數不是全域性物件的屬性。在全域性範圍中,以下宣告建立了此類變數:
let
宣告const
宣告 類宣告
注意,模組的主體不是在全域性範圍內執行的,只有指令碼才是。因此,各種變數的環境形成以下鏈。
9.8 函式宣告和類宣告
函式宣告...
- 是塊作用域,比如
let
。 - 在全域性物件中建立屬性(在全域性作用域中),如
var
。 - 被提升:與其在其作用域中宣告的位置無關,函式宣告總是在作用域中的開頭建立。
以下程式碼演示了函式宣告的提升:
{ // Enter a new scope
console.log(foo()); // OK, due to hoisting
function foo() {
return 'hello';
}
}
複製程式碼
類宣告...
- 是塊作用域。
- 不要在全域性物件上建立屬性。
- 沒有提升。
類沒有被提升可能會令人驚訝,因為在底層引擎下,它們建立了函式。 這種行為的基本原理是它們的 extends
子句的值是通過表示式定義的,並且這些表示式必須在適當的時間執行。
{ // 進入一個新的作用域
const identity = x => x;
// 目前處於 `MyClass` 的TDZ 中
const inst = new MyClass(); // ReferenceError
//注意 `extends` 子句的表示式
class MyClass extends identity(Object) {
}
}
複製程式碼
9.9 編碼樣式: 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: // as // 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中)來實現相同的效果。
9.9.1 替代的方法
對於上面提到的樣式規則,另一種選擇是隻對完全不可變的東西(原始值和凍結的物件)使用 const
。然後有兩種方法:
- 優先
const
:const
標記不可變的繫結。 - 優先
let
:const
標記不可變的值。
我略微傾向於 1,但 2 也可以。