[TOC]
## 9.1 概述
ES6 提供了兩種聲明變量的新方法: `let` 和 `const` ,它們主要取代 ES5 聲明變量的方式:`var` 。
### 9.1.1 let
`let` 與 `var` 類似,但它聲明的變量是具有塊級作用域的(block-scoped),它只存在于當前塊中。 `var` 是函數作用域(function-scoped)。
在下面的代碼中,您可以看到 `let` 聲明的 變量 `tmp` 只存在于 從(A)行開始的塊中:
```js
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`,但是您聲明的變量必須立即初始化,并且該值之后不能更改。
```js
const foo;
// SyntaxError: missing = in const declaration
const bar = 123;
bar = 456;
// TypeError: `bar` is read-only
```
因為 `for-of` 在每次循環迭代中創建一個綁定(變量的存儲空間),所以可以使用 `const` 聲明循環變量
```js
for (const x of ['a', 'b']) {
console.log(x)
}
// Output:
// a
// b
```
### 9.1.3 聲明變量的方式
下表概述了在 ES6 中聲明變量的六種方式(受 [kangax 表](https://twitter.com/kangax/status/567330097603284992)的啟發 ):
| | 提升形式 | 作用域形式 | 創建全局屬性 |
| ---------- | ------------------ | ------------- | ------------ |
| `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` 語句的塊中:
```js
function func() {
if (true) {
const tmp = 123;
}
console.log(tmp); // ReferenceError: tmp is not defined
}
```
相比之下,`var` 聲明的變量是函數級別的:
```js
function func() {
if (true) {
var tmp = 123;
}
console.log(tmp); // 123
}
```
塊作用域意味著您可以在函數中隱藏變量:
```js
function func() {
const foo = 5;
if (···) {
const foo = 10; // shadows outer `foo`
console.log(foo); // 10
}
console.log(foo); // 5
}
```
## 9.3 `const` 創建不可變變量
`let` 創建的變量是可變的:
```js
let foo = 'abc';
foo = 'def';
console.log(foo); // def
```
常量(`const`創建的變量)是不可變的,不能再給它們賦不同的值:
```js
const foo = 'abc';
foo = 'def'; // TypeError
```
**規范細節:更改 `const` 變量總是拋出 `TypeError`**
通常,根據 [SetMutableBinding()](http://www.ecma-international.org/ecma-262/6.0/#sec-declarative-environment-records-setmutablebinding-n-v-s) ,更改不可變綁定僅在嚴格模式下導致異常。 但 `const` 聲明的變量總是產生嚴格的綁定 - 參見 [FunctionDeclarationInstantiation(func, argumentsList)](http://www.ecma-international.org/ecma-262/6.0/#sec-functiondeclarationinstantiation) 。
### 9.3.1 陷阱:`const` 不會使值不可變
`const` 只意味著一個變量總是具有相同的值,但它并不意味著該值本身是不可變的或成為不可變的。 例如, `obj` 是一個常量,但它指向的值是可變的 - 我們可以為它添加一個屬性:
```js
const obj = {};
obj.prop = 123;
console.log(obj.prop); // 123
```
但是,我們不能為 `obj` 分配不同的值:
```js
obj = {}; // TypeError
```
如果您希望 `obj` 的值是不可變的,那么您必須自己處理它。例如, [凍結它](http://speakingjs.com/es5/ch17.html#freezing_objects):
```js
const obj = Object.freeze({});
obj.prop = 123; // TypeError
```
#### 9.3.1.1 陷阱:`Object.freeze()`是淺層的
請記住,`Object.freeze()` 是淺層的,它只會凍結其參數的屬性,而不會凍結其屬性中存儲的對象。例如,對象 `obj` 被凍結:
```js
> 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` 不是。
```js
> obj.foo.qux = 'abc';
> obj.foo.qux
'abc'
```
### 9.3.2 循環體中的 `const`
一旦創建了 `const` 變量,就無法更改它。但這并不意味著您無法重新進入其作用域并賦予新的值重新開始,每次循環都像是一次輪回。例如,通過循環:
```js
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 中,如果獲取或設置變量,則拋出異常:
```js
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 會在初始化后并將結果賦值給變量后結束:
```js
let foo = console.log(foo); // 打印:ReferenceError
```
以下代碼演示死區實際上是臨時的 (基于時間)而不是空間(基于位置):
```js
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` 訪問時間死區中的變量,則會出現異常:
```js
if (true) {
console.log(typeof foo); // ReferenceError (TDZ)
console.log(typeof aVariableThatDoesntExist); // 'undefined'
let foo;
}
```
為什么? 理由如下:`foo` 不是未聲明的,它是未初始化的。你應該意識到它的存在,但你沒有。因此,被警告似乎是可取的。
此外,這種檢查僅對有條件地創建全局變量有用。在正常程序中不需要做的。
#### 9.4.4.1 有條件地創建變量
在有條件地創建變量時,您有兩種選擇。
選項1 - `typeof` 和 `var`:
```js
if (typeof someGlobal === 'undefined') {
var someGlobal = { ··· };
}
```
此選項僅適用于全局范圍(因此不在ES6模塊內)。
選項2 - `window`:
```js
if (!('someGlobal' in window)) {
window.someGlobal = { ··· };
}
```
### 9.4.5 為什么會出現臨時死區(TDZ)?
`const` 和 `let` 產生TDZ,有幾個原因:
* 捕獲編程錯誤:能夠在聲明之前訪問變量很奇怪。如果你這樣做了,可能是意外,你就應該得到警告。
* 對于 `const`:使 `const` 正常工作很困難。[引用Allen Wirfs-Brock](https://mail.mozilla.org/pipermail/es-discuss/2012-September/024996.html):“TDZs ......為const提供了理性的語義。 對該主題進行了重要的技術討論,并且TDZ 成為最佳解決方案。“ `let` 也有一個臨時死區,這樣 `let` 和 `const` 之間的切換不會以意料之外的方式改變行為。
* 面向未來的防護:JavaScript最終可能會有一些防護,一種在運行時強制執行變量具有正確值的機制(想想運行時類型檢查)。如果變量的值在聲明之前 `undefined`,那么該值可能與 其保護所給出的保證 相沖突。
### 9.4.6 進一步閱讀
本節的來源:
* “[let/const的性能問題](https://esdiscuss.org/topic/performance-concern-with-let-const) ”
* “[Bug 3009 -在TDZ變量上的 typeof](https://bugs.ecmascript.org/show_bug.cgi?id=3009) ”
## 9.5 循環頭部中的 `let` 和 `const`
以下循環允許您在其頭部聲明變量:
* `for
`
* `for-in
`
* `for-of
`
要進行聲明,可以使用`var`, `let` 或 `const` 。 他們每個都有不同的影響,我將在下面解釋。
### 9.5.1 `for` 循環
在for循環頭部中的`var` 變量為該變量創建單個綁定 (存儲空間):
```js
const arr = [];
for (var i=0; i < 3; i++) {
arr.push(() => i);
}
arr.map(x => x()); // [3,3,3]
```
三個箭頭函數體中的每個`i` 指向相同的綁定,這就是它們都返回相同值的原因。
`let` 變量,則為每個循環迭代創建一個新綁定:
```js
const arr = [];
for (let i=0; i < 3; i++) {
arr.push(() => i);
}
arr.map(x => x()); // [0,1,2]
```
這一次,每個 `i` 指的是一個特定迭代的綁定,并保留當時的值。因此,每個箭頭函數返回不同的值。
`const`像 `var` 一樣的工作,但是你不能改變`const` 變量的初始值:
```js
// TypeError: Assignment to constant variable
// (due to i++)
for (const i=0; i<3; i++) {
console.log(i);
}
```
為每次迭代獲取新的綁定起初可能看起來很奇怪,但是每當使用循環 創建引用循環變量的函數時,它非常有用,后面的小節將對此進行解釋。
**`for` 循環:規范中的每個迭代綁定**
[`for` 循環的求值](http://www.ecma-international.org/ecma-262/6.0/#sec-for-statement-runtime-semantics-labelledevaluation) 將`var` 作為第二種情況處理,并將 `let/const` 作為第三種情況。 只有 `let` 變量被添加到列表 `perIterationLets`(步驟9)中,它作為倒數第二個參數 `perIterationBindings` 傳遞給`ForBodyEvaluation()` 。
### 9.5.2 `for-of` 循環和 `for-in` 循環
在 `for-of` 循環中,`var`創建單個綁定:
```js
const arr = [];
for (var i of [0, 1, 2]) {
arr.push(() => i);
}
arr.map(x => x()); // [2,2,2]
```
`const` 每次迭代創建一個不可變的綁定:
```js
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](http://www.ecma-international.org/ecma-262/6.0/#sec-runtime-semantics-forin-div-ofbodyevaluation-lhs-stmt-iterator-lhskind-labelset)處理。在步驟5.b中,創建一個新環境,并通過[BindingInstantiation](http://www.ecma-international.org/ecma-262/6.0/#sec-runtime-semantics-bindinginstantiation)為其添加綁定(對于`let`可變的,對于`const`不可變的)。當前迭代值存儲在變量`nextValue`,用于以兩種方式之一初始化綁定:
單變量聲明(步驟5.hi):通過 [`InitializeReferencedBinding`](http://www.ecma-international.org/ecma-262/6.0/#sec-initializereferencedbinding) 處理
解構(步驟5.i.iii):通過[一個 BindingInitialization 情況](http://www.ecma-international.org/ecma-262/6.0/#sec-for-in-and-for-of-statements-runtime-semantics-bindinginitialization) ( ForDeclaration )來處理,它調用[另一個BindingInitialization](http://www.ecma-international.org/ecma-262/6.0/#sec-destructuring-binding-patterns-runtime-semantics-bindinginitialization) ( BindingPattern )的情況。
### 9.5.3 為什么每次迭代綁定是有用的?
以下是顯示三個鏈接的HTML頁面:
1. 如果單擊“yes”,則將其翻譯為“ja”。
2. 如果單擊“no”,則將其翻譯為“nein”。
3. 如果單擊“perhaps”,則會將其翻譯為“vielleicht”。
```js
<!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` 與參數具有相同名稱的變量,則會出現靜態(加載時)錯誤:
```js
function func(arg) {
let arg; // static error: duplicate declaration of `arg`
}
```
在塊內執行相同操作會影響參數:
```js
function func(arg) {
{
let arg; // shadows parameter `arg`
}
}
```
相比之下, `var` 與參數同名的變量什么都不做,就像在同一作用域中重新聲明 `var`變量一樣。
```js
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` 語句,并受臨時死區的影響:
```js
// 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 參數默認值看不到主體的作用域
參數默認值的范圍與主體作用域分開(前者圍繞后者)。這意味著在參數默認值中定義的方法或函數不會看到主體的局部變量
```js
const foo = 'outer';
function bar(func = x => foo) {
const foo = 'inner'; // 不會被看到哦~
console.log(func()); // outer
}
bar();
```
## 9.7 全局對象
JavaScript的[全局對象](http://speakingjs.com/es5/ch16.html#global_object) (Web瀏覽器中的 `window` ,Node.js中的 `global`)更像是一個bug而不是一個功能,特別是在性能方面。這就是ES6 引入區別的原因:
* 全局對象的所有屬性都是全局變量。在全局范圍中,以下聲明創建了這樣的屬性:
* `var`聲明
* 函數聲明
* 但是現在還有全局變量不是全局對象的屬性。在全局范圍中,以下聲明創建了此類變量:
`let` 聲明
`const` 聲明
類聲明
注意,模塊的主體不是在全局范圍內執行的,只有腳本才是。因此,各種變量的環境形成以下鏈。

## 9.8 函數聲明和類聲明
函數聲明...
* 是塊級作用域,比如 `let`。
* 在全局對象中創建屬性(在全局作用域中),如`var` 。
* 被提升:與其在其作用域中聲明的位置無關,函數聲明總是在作用域中的開頭創建。
以下代碼演示了函數聲明的提升:
```js
{ // Enter a new scope
console.log(foo()); // OK, due to hoisting
function foo() {
return 'hello';
}
}
```
類聲明...
* 是塊級作用域。
* 不要在全局對象上創建屬性。
* 沒有提升。
類沒有被提升可能會令人驚訝,因為在底層引擎下,它們創建了函數。 這種行為的基本原理是它們的 `extends` 子句的值是通過表達式定義的,并且這些表達式必須在適當的時間執行。
```js
{ // 進入一個新的作用域
const identity = x => x;
// 目前處于 `MyClass` 的TDZ 中
const inst = new MyClass(); // ReferenceError
//注意 `extends` 子句的表達式
class MyClass extends identity(Object) {
}
}
```
## 9.9 編碼樣式: const與let對比var
我建議總是使用 `let` 或 `const`:
1. 使用 `const`:只要變量永遠不會改變其值,就可以使用它。 換句話說:變量不應該是賦值的左邊,也不應該是++或-的操作數。允許更改 `const` 變量引用的對象:
```js
const foo = {};
foo.prop = 123; // OK
```
你甚至可以在 `for-of` 循環中使用`const`,因為每次循環迭代都會創建一個(不可變的)綁定:
```js
for (const x of ['a', 'b']) {
console.log(x);
}
// Output:
// as
// b
```
在`for-of` 循環體內,`x`不能改變。
2. 稍后會更改變量的初始值時,則應該使用 `let` 。
```js
let counter = 0; // initial value
counter++; // change
let obj = {}; // initial value
obj = { foo: 123 }; // change
```
3. 避免 `var`。
如果遵循這些規則,`var` 將僅出現在遺留代碼中,作為需要仔細重構的信號。
`var` 會做一件`let` 和 `const` 不會做的事情:通過它聲明的變量成為了全局對象的屬性。然而,這通常不是一件好事。您可以通過分配到 `window` (在瀏覽器中)或 `global`(在Node.js中)來實現相同的效果。
### 9.9.1 替代的方法
對于上面提到的樣式規則,另一種選擇是只對完全不可變的東西(原始值和凍結的對象)使用 `const` 。然后有兩種方法:
1. 優先 `const` : `const` 標記不可變的綁定。
2. 優先 `let` :`const` 標記不可變的值。
我略微傾向于 1,但 2 也可以。
- 關于本書
- 目錄簡介
- 關于這本書你需要知道的
- 序
- 前言
- I 背景
- 1. About ECMAScript 6 (ES6)
- 2. 常見問題:ECMAScript 6
- 3. 一個JavaScript:在 ECMAScript 6 中避免版本化
- 4. 核心ES6特性
- II 數據
- 5. New number and Math features
- 6. 新的字符串特性
- 7. Symbol
- 8. Template literals
- 第9章 變量與作用域
- 第10章 解構
- 第11章 參數處理
- III 模塊化
- 12. ECMAScript 6中的可調用實體
- 13. 箭頭函數
- 14. 除了類之外的新OOP特性
- 15. 類
- 16. 模塊
- IV 集合
- 17. The for-of loop
- 18. New Array features
- 19. Maps and Sets
- 20. 類型化數組
- 21. 可迭代對象和迭代器
- 22. 生成器( Generator )
- V 標準庫
- 23. 新的正則表達式特性
- 24. 異步編程 (基礎知識)
- 25. 異步編程的Promise
- VI 雜項
- 26. Unicode in ES6
- 27. 尾部調用優化
- 28 用 Proxy 實現元編程
- 29. Coding style tips for ECMAScript 6
- 30. 概述ES6中的新內容
- 注釋
- ES5過時了嗎?
- ==個人筆記==