## 34.解構
> 原文: [http://exploringjs.com/impatient-js/ch_destructuring.html](http://exploringjs.com/impatient-js/ch_destructuring.html)
>
> 貢獻者:[Kavelaa](https://github.com/Kavelaa)
### 34.1 第一次嘗試解構
通過普通賦值方式,你一次只能獲取數據中的一個元素,例如,通過:
```js
const arr = ['a', 'b', 'c'];
const x = arr[0]; // 只能獲取第一個
const y = arr[1]; // 只能獲取第二個
```
通過解構方式,你可以同時獲取數據中的幾個元素,通過在接收數據的位置進行模式匹配。在上方的代碼的`=`左側就是這樣一個位置。在下方的代碼中,行A中的方括號就是一個解構模式:
```js
const arr = ['a', 'b', 'c'];
const [x, y] = arr; // (A)
assert.equal(x, 'a');
assert.equal(y, 'b');
```
這段代碼做了和之前的代碼同樣的事。
注意,這種方式比整個數據要“小巧”:我們只獲取我們需要的數據元素。(譯者注:冒號后面的話才是核心)
### 34.2 構造與提取
為了明白什么是解構,假想JavaScript有兩種截然相反的操作:
+ 你可以 *構造* 復合數據,例如通過設置屬性和對象文字的方式。
+ 你可以從復合數據中 *提取* 數據,例如通過獲取屬性的方式。
構造數據看起來像這樣:
```js
// 一次添加一個屬性
const jane1 = {};
jane1.first = 'Jane';
jane1.last = 'Doe';
// 一次添加很多屬性
const jane2 = {
first: 'Jane',
last: 'Doe',
};
assert.deepEqual(jane1, jane2);
```
提取數據看起來像這樣:
```js
const jane = {
first: 'Jane',
last: 'Doe',
};
// 一次獲取一個屬性
const f1 = jane.first;
const l1 = jane.last;
assert.equal(f1, 'Jane');
assert.equal(l1, 'Doe');
// 一次獲取多個屬性(新方式!)
const {first: f2, last: l2} = jane; // (A)
assert.equal(f2, 'Jane');
assert.equal(l2, 'Doe');
```
行A中的操作方式是全新的:我們聲明兩個變量`f2`和`l2`,并通過 *解構* 方式(多值提取)初始化它們。
下方行A的部分是一個 *解構* 模式:
```js
{first: f2, last: l2}
```
解構模式在語法上類似于用于多值構造的寫法。但是它們出現在接收數據的地方(在賦值語句的左邊),而不是創建數據的地方(在賦值語句的右邊)。
### 34.3 我們在哪里可以使用解構?
解構模式可用于“數據接收位置”,例如:
+ 變量聲明:
```js
const [a] = ['x'];
assert.equal(a, 'x');
let [b] = ['y'];
assert.equal(b, 'y');
```
+ 賦值語句:
```js
let b;
[b] = ['z'];
assert.equal(b, 'z');
```
+ 參數定義:
```js
const f = ([x]) => x;
assert.equal(f(['a']), 'a');
```
注意,變量的定義包括在`for-of`循環中的`const`和`let`聲明:
```js
const arr = ['a', 'b'];
for (const [index, element] of arr.entries()) {
console.log(index, element);
}
// Output:
// 0, 'a'
// 1, 'b'
```
在接下來的兩部分中,我們將深入研究兩種類型的解構:對象解構和數組解構。
### 34.4 對象的解構
*對象解構* 允許您批量提取屬性值,通過看起來像對象寫法的模式:
```js
const address = {
street: 'Evergreen Terrace',
number: '742',
city: 'Springfield',
state: 'NT',
zip: '49007',
};
const { street: s, city: c } = address;
assert.equal(s, 'Evergreen Terrace');
assert.equal(c, 'Springfield');
```
你可以把這種模式想象成一個透明的表格,你把它放在數據上面:模式的鍵`street`在數據中有一個對應的鍵名匹配。因此,數據的值`'Evergreen Terrace'`被賦值給了模式的變量`s`。
你也可以對象解構原始類型的值:
```js
const {length: len} = 'abc';
assert.equal(len, 3);
```
你還可以對象解構數組:
```js
const {0:x, 2:y} = ['a', 'b', 'c'];
assert.equal(x, 'a');
assert.equal(y, 'c');
```
為什么這樣可以?因為[數組索引也是屬性](https://exploringjs.com/impatient-js/ch_arrays.html#array-indices)。
#### 34.4.1 屬性值縮寫
對象寫法支持屬性值縮寫,對象解構模式也支持:
```js
const { street, city } = address;
assert.equal(street, 'Evergreen Terrace');
assert.equal(city, 'Springfield');
```
#### 34.4.2 Rest屬性
在對象寫法中,你可以擁有擴展運算符。在對象模式中,可以使用rest屬性(必須放在最后):
```js
const obj = { a: 1, b: 2, c: 3 };
const { a: propValue, ...remaining } = obj; // (A)
assert.equal(propValue, 1);
assert.deepEqual(remaining, {b:2, c:3});
```
一個rest參數,例如`remaining`(A行)被賦予一個對象,該對象具有模式中沒有提到鍵的所有數據屬性。
`remaining`也可以看作是從`obj`中非破壞性地刪除屬性`a`的結果。(譯者注:不再需要`a`屬性,但不想改變原對象`obj`,得到一個新的不包含`a`的數組`remaining`)
#### 34.4.3 語法陷阱:通過對象解構來賦值
如果我們在賦值過程中使用對象解構,我們就會面臨語法歧義帶來的陷阱——你不能用大括號來開始一個語句,因為JavaScript會認為你在開始一個代碼塊:
```js
let prop;
assert.throws(
() => eval("{prop} = { prop: 'hello' };"),
{
name: 'SyntaxError',
message: 'Unexpected token =',
});
```
>  **為什么`eval()`**?
>
> `eval()`會延遲解析(因此也會延遲`SyntaxError`),直到執行`assert.throw()`的回調。如果我們不使用它,在解析這段代碼時就會得到一個錯誤,`assert.throw()`甚至不會執行。
解決辦法是把整個賦值語句放在括號里:
```js
let prop;
({prop} = { prop: 'hello' });
assert.equal(prop, 'hello');
```
### 34.5 數組解構
*數組解構* 讓你通過像數組寫法的方式,批量提取數組元素:
```js
const [x, y] = ['a', 'b'];
assert.equal(x, 'a');
assert.equal(y, 'b');
```
在數組的模式中,你可以通過提交空值的方式來跳過元素:
```js
const [, x, y] = ['a', 'b', 'c']; // (A)
assert.equal(x, 'b');
assert.equal(y, 'c');
```
在A行,數組模式的第一個元素為空,這就是索引為0的數組元素被忽略的原因。
#### 34.5.1 數組解構可以用于任何可遍歷的數據結構
數組解構可以用于任何可遍歷的值,而不僅僅是數組:
```js
// Sets數據結構是iterable的
const mySet = new Set().add('a').add('b').add('c');
const [first, second] = mySet;
assert.equal(first, 'a');
assert.equal(second, 'b');
// 字符串數據結構是iterable的
const [a, b] = 'xyz';
assert.equal(a, 'x');
assert.equal(b, 'y');
```
#### 34.5.2 Rest參數
在數組文字中,你可以使用擴展運算符。在數組模式中,你可以使用rest參數(必須放在最后):
```js
const [x, y, ...remaining] = ['a', 'b', 'c', 'd']; // (A)
assert.equal(x, 'a');
assert.equal(y, 'b');
assert.deepEqual(remaining, ['c', 'd']);
```
rest元素變量,例如`remaining`(A行)被賦予一個數組,其中包含尚未提到的已解構值的所有元素。
### 34.6 解構的一些例子
#### 34.6.1 數組解構:交換變量的值
你可以使用數組解構來交換兩個變量的值,而不需要臨時變量:
```js
let x = 'a';
let y = 'b';
[x,y] = [y,x]; // swap
assert.equal(x, 'b');
assert.equal(y, 'a');
```
#### 34.6.2 數組解構:用于會返回數組的操作
數組解構在用于會返回數組的操作時非常有用。例如,正則表達式方法`.exec()`:
```js
// Skip the element at index 0 (the whole match):
const [, year, month, day] =
/^([0-9]{4})-([0-9]{2})-([0-9]{2})$/
.exec('2999-12-31');
assert.equal(year, '2999');
assert.equal(month, '12');
assert.equal(day, '31');
```
#### 34.6.3 對象解構:多個返回值
解構對函數返回多個值的情況非常有用——要么打包為數組,要么打包為對象。
假想一個函數`findElement()`,用于尋找一個數組內的元素:
```js
findElement(array, (value, index) => ?boolean expression?)
```
它的第二個參數是一個函數,該函數接收元素的值和索引,并返回一個布爾值,指示這是否是調用者正在尋找的元素。
我們現在面臨一個難題:`findElement()`應該返回它找到的元素的值還是索引的值?一個解決方案是創建兩個單獨的函數,但這會導致代碼重復,因為這兩個函數非常相似。
下面的實現通過返回一個同時包含找到的元素的索引和值的對象,來避免重復:
```js
function findElement(arr, predicate) {
for (let index=0; index < arr.length; index++) {
const value = arr[index];
if (predicate(value)) {
// 有所發現的話
return { value, index };
}
}
// 什么都沒找到的話
return { value: undefined, index: -1 };
}
```
解構幫助我們處理`findElement()`的結果:
```js
const arr = [7, 8, 6];
const {value, index} = findElement(arr, x => x % 2 === 0);
assert.equal(value, 8);
assert.equal(index, 1);
```
當我們使用屬性的鍵名時,我們聲明值和索引時的順序并不重要:
```js
const {index, value} = findElement(arr, x => x % 2 === 0);
```
更妙的是,如果我們只對以下兩種結果中的一種感興趣,解構也能很好地幫助我們:
```js
const arr = [7, 8, 6];
const {value} = findElement(arr, x => x % 2 === 0);
assert.equal(value, 8);
const {index} = findElement(arr, x => x % 2 === 0);
assert.equal(index, 1);
```
這些方便結合在一起,使得這種處理多個返回值的方法非常通用。
### 34.7 如果一個模式的一部分不匹配,會發生什么?
如果一個模式的一部分不匹配,會發生什么?如果使用非批處理操作符也會發生同樣的事情:您將得到`undefined`的結果。
#### 34.7.1 對象解構時缺少屬性
如果對象模式中的屬性在右側沒有匹配項,則得到`undefined`:
```js
const {prop: p} = {};
assert.equal(p, undefined);
```
#### 34.7.2 數組解構時缺少元素
如果數組模式中的元素在右側沒有匹配項,則得到`undefined`:
```js
const [x] = [];
assert.equal(x, undefined);
```
### 34.8 什么值無法被解構?
#### 34.8.1 你不能對`undefined`和`null`使用對象解構
只有當要解構的值未定義(`undefined`)或為空(`null`)時,對象解構才會失敗。也就是說,當通過點操作符訪問屬性時也會失敗。
```js
assert.throws(
() => { const {prop} = undefined; },
{
name: 'TypeError',
message: "Cannot destructure property `prop` of " +
"'undefined' or 'null'.",
}
);
assert.throws(
() => { const {prop} = null; },
{
name: 'TypeError',
message: "Cannot destructure property `prop` of " +
"'undefined' or 'null'.",
}
);
```
#### 34.8.2 你不能對無法遍歷的值進行數組解構
數組解構要求解構值是可遍歷的。因此,不能對未定義(`undefined`)或者空(`null`)的數組進行解構,同樣你也不能解構無法遍歷的對象:
```js
assert.throws(
() => { const [x] = {}; },
{
name: 'TypeError',
message: '{} is not iterable',
}
);
```
>  **測試:基本**
>
> 請看 [quiz app](https://exploringjs.com/impatient-js/ch_quizzes-exercises.html#quizzes)
### 34.9 (高級用法)
余下部分皆為高級用法。
### 34.10 默認值
一般情況下,如果一個模式沒有匹配,則將對應的變量設置為`undefined`:
```js
const {prop: p} = {};
assert.equal(p, undefined);
```
如果你想要設定一個同樣用途的不同的值,你需要指定一個初始值(通過`=`):
```js
const {prop: p = 123} = {}; // (A)
assert.equal(p, 123);
```
在行A中,我們指定`p`的默認值為`123`。該默認值會生效,因為要解構的的數據中沒有名為`prop`的屬性。
#### 34.10.1 默認值在數組解構中的用法
在這里,我們有兩個默認值被賦給變量`x`和`y`,因為被解構的數組中不存在相應的元素。
```js
const [x=1, y=2] = [];
assert.equal(x, 1);
assert.equal(y, 2);
```
給數組模式的第一個元素的默認值是`1`,給第二個元素的值是`2`。
#### 34.10.2 默認值對象解構中的用法
你也可以為對象解構指定默認值:
```js
const {first: f='', last: l=''} = {};
assert.equal(f, '');
assert.equal(l, '');
```
屬性鍵`first`和屬性鍵`last`都不存在于被解構的對象中。因此,默認值會生效。
使用屬性值縮寫,代碼會變得更簡單:
```js
const {first='', last=''} = {};
assert.equal(first, '');
assert.equal(last, '');
```
### 34.11 參數定義和解構的相似性
考慮到我們在本章所學到的內容,參數定義與數組模式(rest元素、默認值等)有很多共同點。事實上,以下兩個函數聲明是等價的:
```js
function f1(?pattern1?, ?pattern2?) {
// ···
}
function f2(...args) {
const [?pattern1?, ?pattern2?] = args;
// ···
}
```
### 34.12 嵌套解構
到目前為止,我們只在解構模式中使用變量作為 *賦值目標* (數據接收方)。但是你也可以使用模式作為賦值目標,這使你能夠將模式嵌套到任意深度:
```js
const arr = [
{ first: 'Jane', last: 'Bond' },
{ first: 'Lars', last: 'Croft' },
];
const [, {first}] = arr; //(A)
assert.equal(first, 'Lars');
```
在A行的數組模式中,有一個數組嵌套,位于索引1。(譯者注:即`{first}`,實際上作者并沒有注釋A行在何處,是我后來添加上去的。或許作者是故意的呢?哈哈。)
嵌套模式不太容易理解,所以最好適當地使用。
>  **測試:高級用法**
>
> 請看[quiz app](https://exploringjs.com/impatient-js/ch_quizzes-exercises.html#quizzes)
- I.背景
- 1.關于本書(ES2019 版)
- 2.常見問題:本書
- 3. JavaScript 的歷史和演變
- 4.常見問題:JavaScript
- II.第一步
- 5.概覽
- 6.語法
- 7.在控制臺上打印信息(console.*)
- 8.斷言 API
- 9.測驗和練習入門
- III.變量和值
- 10.變量和賦值
- 11.值
- 12.運算符
- IV.原始值
- 13.非值undefined和null
- 14.布爾值
- 15.數字
- 16. Math
- 17. Unicode - 簡要介紹(高級)
- 18.字符串
- 19.使用模板字面值和標記模板
- 20.符號
- V.控制流和數據流
- 21.控制流語句
- 22.異常處理
- 23.可調用值
- VI.模塊化
- 24.模塊
- 25.單個對象
- 26.原型鏈和類
- 七.集合
- 27.同步迭代
- 28.數組(Array)
- 29.類型化數組:處理二進制數據(高級)
- 30.映射(Map)
- 31. WeakMaps(WeakMap)
- 32.集(Set)
- 33. WeakSets(WeakSet)
- 34.解構
- 35.同步生成器(高級)
- 八.異步
- 36. JavaScript 中的異步編程
- 37.異步編程的 Promise
- 38.異步函數
- IX.更多標準庫
- 39.正則表達式(RegExp)
- 40.日期(Date)
- 41.創建和解析 JSON(JSON)
- 42.其余章節在哪里?