## 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 =',
});
```
> ![](https://raw.githubusercontent.com/apachecn/impatient-js-zh/master/docs/img/6ddc665b06b04cbcdf4bc6a9c514a8c4.svg?sanitize=true) **为什么`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',
}
);
```
> ![](https://raw.githubusercontent.com/apachecn/impatient-js-zh/master/docs/img/bf533f04c482f83bfc407f318306f995.svg?sanitize=true) **测试:基本**
>
> 请看 [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行在何处,是我后来添加上去的。或许作者是故意的呢?哈哈。)
嵌套模式不太容易理解,所以最好适当地使用。
> ![](https://raw.githubusercontent.com/apachecn/impatient-js-zh/master/docs/img/bf533f04c482f83bfc407f318306f995.svg?sanitize=true) **测试:高级用法**
>
> 请看[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.其余章节在哪里?