ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
## 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)