多应用+插件架构,代码干净,二开方便,首家独创一键云编译技术,文档视频完善,免费商用码云13.8K 广告
[TOC] # 第10章 解构(Destructuring) ## 10.1 概览 解构(Destructuring) 是一种从数据中提取值的便捷方式,这些数据存储在(可能嵌套的)对象和数组中。解构可以用在接收数据的地方(比如赋值操作的左边)。提取的具体方式取决于模式(看后面的例子就明白啦)。 ### 10.1.1 对象解构(Object destructuring) 解构对象: ```js const obj = { first: 'Jane', last: 'Doe' }; const {first: f, last: l} = obj; // f = 'Jane'; l = 'Doe' // {prop} 是 {prop: prop} 的缩写 const {first, last} = obj; // first = 'Jane'; last = 'Doe' ``` 解构能帮助处理返回值: ```js const obj = { foo: 123 }; const {writable, configurable} = Object.getOwnPropertyDescriptor(obj, 'foo'); console.log(writable, configurable); // true true ``` ### 10.1.2 数组解构(Array destructuring) 数组解构(作用于所有可迭代的值): ```js const iterable = ['a', 'b']; const [x, y] = iterable; // x = 'a'; y = 'b' ``` 解构能帮助处理返回值: ```js const [all, year, month, day] = /^(\d\d\d\d)-(\d\d)-(\d\d)$/ .exec('2999-12-31'); ``` ### 10.1.3 解构可以用在什么地方? 解构可以用在以下地方: ```js // 变量声明: const [x] = ['a']; let [x] = ['a']; var [x] = ['a']; // 赋值: [x] = ['a']; // 参数定义: function f([x]) { ··· } f(['a']); ``` 你还可以在`for-of`循环里进行解构: ```js const arr = ['a', 'b']; for (const [index, element] of arr.entries()) { console.log(index, element); } // Output: // 输出: // 0 a // 1 b ``` ## 10.2 背景:构造数据 vs 提取数据 为了充分理解什么是解构,我们先看看它所在的更广义的上下文环境。 JavaScript 有以下操作来构造数据: ```js const obj = {}; obj.first = 'Jane'; obj.last = 'Doe'; ``` 并且有以下操作来提取数据: ```js const f = obj.first; const l = obj.last; ``` 注意,我们提取数据的时候用了跟构造数据时一样的语法。 还有更好的构造语法——对象字面量: ```js const obj = { first: 'Jane', last: 'Doe' }; ``` 在ES6之前,没有相应的提取数据的机制。在 ECMAScript 6 里解构允许使用同样的语法提取数据,这种语法在提取数据时叫做"对象模式"。如下例,赋值符的左边: ```js const { first: f, last: l } = obj; ``` 就像对象字面量允许同时创建多个属性一样,对象模式允许我们同时提取多个属性。 你也可以通过模式解构数组: ```js const [x, y] = ['a', 'b']; // x = 'a'; y = 'b' ``` ## 10.3 模式(Patterns) 以下是解构相关的两个部分: * 解构源(Destructuring source): 被解构的数据。比如,解构赋值的右边。 * 解构目标(Destructuring target): 解构的目标。比如,解构赋值的左边。 解构目标有以下三种模式: * 赋值目标(Assignment target)。 比如: x * 在变量声明和参数定义里,只允许对变量的引用。在解构赋值里,你有更多选择,稍后会进行解释。 * 对象模式(Object pattern)。 比如:{ first: «pattern», last: «pattern» } * 一个对象模式的组成部分是属性,属性的值还是模式(可递归)。 * 数组模式(Array pattern)。 比如: [ «pattern», «pattern» ] * 数组模式的组成部分是元素,元素还是模式(可递归)。 这意味着能够以任意的深度嵌套模式: ```js const obj = { a: [{ foo: 123, bar: 'abc' }, {}], b: true }; const { a: [{foo: f}] } = obj; // f = 123 ``` ### 10.3.1 按需挑选模式 假如你要解构一个对象,你只需要写你想要的属性: ```js const { x: x } = { x: 7, y: 3 }; // x = 7 ``` 假如你要解构一个数组,你可以选择只提取前面的部分: ```js const [x,y] = ['a', 'b', 'c']; // x='a'; y='b'; ``` ## 10.4 模式是如何访问值的内部结构的? 在 `pattern = someValue` 这个赋值里, `pattern` 是如何访问 `someValue` 内部的呢? ### 10.4.1 对象模式强制将值转化成对象处理 对象模式在访问属性之前会强制将解构源转化成对象。这意味着它能处理原始类型值(primitive values): ```js const {length : len} = 'abc'; // len = 3 const {toString: s} = 123; // s = Number.prototype.toString ``` #### 10.4.1.1 有时候,无法对值进行对象解构 强制转化成对象的操作并不是通过 `Object()` 实现的,而是通过内部操作 `ToObject()`。 `Object()` 永远都不会失败: ```js > typeof Object('abc') 'object' > var obj = {}; > Object(obj) === obj true > Object(undefined) {} > Object(null) {} ``` 当遇到 undefined 或 null 时, ToObject() 会抛一个 TypeError 错误。因此,下面的解构在访问任何属性之前就失败了: ```js const { prop: x } = undefined; // TypeError const { prop: y } = null; // TypeError ``` 所以,你可以使用空对象模式 `{}` 检查一个值能否强制转换成对象。我们已经知道,只有 `undefined` 和 `null` 不能: ```js ({} = [true, false]); // OK,数组强制转换成对象 ({} = 'abc'); // OK,字符串强制转换成对象 ({} = undefined); // TypeError ({} = null); // TypeError ``` 以上表达式外面的圆括号是必须的,因为在 JavaScript 里,声明不可以用花括号开始([细节稍后解释](###))。 ### 10.4.2 数组模式对可迭代的值都可以生效 数组解构使用了迭代器来获取解构源的元素。因此,你可以数组解构任何可迭代的值。我们来看几个可迭代值的例子。 字符串是可迭代的: ```js const [x,...y] = 'abc'; // x='a'; y=['b', 'c'] ``` 别忘了,字符串的迭代器返回的是代码点(“Unicode 字符”, 21 位),而不是代码单元(“JavaScript 字符”, 16 位)。(更多关于 Unicode 的信息,参考“Speaking Javascript”这本书里的“**第 24 章 Unicode 与 JavaScript**”。) 比如: ```js const [x,y,z] = 'a\uD83D\uDCA9c'; // x='a'; y='\uD83D\uDCA9'; z='c' ``` 你不能通过索引访问 `Set` 的元素,但是你可以通过迭代器来访问。因此,数组解构也支持 `Set`: ```js const [x,y] = new Set(['a', 'b']); // x='a'; y='b’; ``` `Set` 迭代器按照插入顺序返回元素,这也是解释了为什么上面的解构每次返回的结果都相同。 #### 10.4.2.1 有时候,无法对值进行数组解构 当一个值有 `Symbol.iterator` 方法,且这个方法能返回一个对象时,它就是可迭代的。假如被解构的值不可迭代,数组解构就会抛出 `TypeError` 的错误: ```js let x; [x] = [true, false]; // OK,数组是可迭代的 [x] = 'abc'; // OK, 字符串是可迭代的 [x] = { * [Symbol.iterator]() { yield 1 } }; // OK,可迭代 [x] = {}; // TypeError,空对象不可迭代 [x] = undefined; // TypeError,不可迭代 [x] = null; // TypeError,不可迭代 ``` 访问数组元素之前,就会抛出 TypeError ,这意味着你可以用空数组模式 [] 检查一个值是不是可迭代的: ```js [x] = {}; // TypeError,空对象不可迭代 [x] = undefined; // TypeError,不可迭代 [x] = null; // TypeError,不可迭代 ``` ## 10.5 默认值(Default values) 默认值(Default values) 是模式的一个特性: 假如有一部分(一个对象属性或者一个数组元素)在解构源中没有匹配到,那么它就会被匹配成: 1. 它的 默认值 (如果指定了的话) 2. `undefined` (没指定时) 也就是说,提供一个默认值是可选的操作。 来看一个例子。在下面的解构中,下标为 0 的元素在右边没有匹配值。因此,解构会继续将 x 匹配成 3,结果就是 x 被设置成了 3。 ```js const [x=3, y] = []; // x = 3; y = undefined ``` 你也可以在对象模式中使用默认值: ```js const {foo: x=3, bar: y} = {}; // x = 3; y = undefined ``` ### 10.5.1.1 undefined 会触发默认值 当某一部分有匹配值,并且匹配值是 `undefined` 时,也会最终匹配到默认值: ```js const [x=1] = [undefined]; // x = 1 const {prop: y=2} = {prop: undefined}; // y = 2 ``` 这一行为的根本原因将会在**下一章的参数默认值一节**中解释。 ### 10.5.1.2 默认值是按需计算的 默认值本身只在需要时(即被触发时)进行计算。也就是说,下面的解构: ```js const {prop: y=someFunc()} = someValue; ``` 等价于: ```js let y; if (someValue.prop === undefined) { y = someFunc(); } else { y = someValue.prop; } ``` 假如你用 console.log()的话,可以观察到这一点: ```js > function log(x) { console.log(x); return 'YES' } > const [a=log('hello')] = []; hello > a 'YES' > const [b=log('hello')] = [123]; > b 123 ``` 在第二个解构中,默认值不会被触发,log() 不会被调用。 ### 10.5.1.3 默认值可以指向模式中的其它变量 默认值可以指向任何变量,包括同一模式下的其它变量: ```js const [x=3, y=x] = []; // x=3; y=3 const [x=3, y=x] = [7]; // x=7; y=7 const [x=3, y=x] = [7, 2]; // x=7; y=2 ``` 但是,要注意顺序:变量 x 和 y 是从左到右声明的,假如在变量声明之前访问它,就会产生 ReferenceError: ```js const [x=y, y=3] = []; // ReferenceError ``` ### 10.5.1.4 模式的默认值 目前我们只看到了变量的默认值,其实也可以给模式设定默认值: ```js const [{ prop: x } = {}] = []; ``` 这样做的意义是什么呢?我们来回顾一下默认值的规则: > 假如在解构源中没有匹配到,解构会继续匹配默认值[…]。 因为匹配不到下标为 0 的元素,解构就会继续下面的匹配: ```js const { prop: x } = {}; // x = undefined ``` 如果把模式 { prop: x} 替换成变量 pattern,就更容易理解了: ```js const [pattern = {}] = []; ``` ### 10.5.5 更复杂的默认值 我们进一步探索模式的默认值。在下面的例子里,通过默认值 `{ prop: 123 }` 给 `x` 赋值: 因为下标为 0 的数组元素在右侧没有匹配,解构就会继续下面的匹配,最终 `x` 被设为 `123`。 ```js const { prop: x } = { prop: 123 }; // x = 123 ``` 但是,在下面这种情况下,即使右侧的默认值里有下标为 0 的元素,x 也不会被赋值。因为这个默认值不会被触发。 ```js const [{ prop: x } = { prop: 123 }] = [{}]; ``` 在这种情况下,解构会继续下面的匹配: ```js const { prop: x } = {}; // x = undefined ``` 因此,假如你希望无论是对象还是属性缺失,`x` 都默认为 `123` 的话,你需要给 `x 本身指定一个默认值: ```js const [{ prop: x=123 } = {}] = [{}]; ``` 这样的话,解构就会像下面这样继续进行,无论右侧是 `[{}]` 还是 `[]`。 ```js const { prop: x=123 } = {}; // x = 123 ``` ## 还有疑问? [稍后会有一节](###) 从另一个角度——算法角度——来解释解构。也许能让你有新的见解 ## 10.6 对象解构的更多特性 ### 10.6.1 属性值缩写 属性值缩写是对象字面量的一个特性:假如属性值用变量表示,且变量与属性的键同名,你就可以省略键。对于解构,也同样适用: ```js const { x, y } = { x: 11, y: 8 }; // x = 11; y = 8 ``` 上述声明等价于: ```js const { x: x, y: y } = { x: 11, y: 8 }; ``` 你也可以将默认值与属性值缩写结合起来: ```js const { x, y = 1 } = {}; // x = undefined; y = 1 ``` ### 10.6.2 可计算的属性键(Computed property keys) 可计算的属性键是对象字面量的另一个特点,这也同样适用于解构。通过将表达式放进方括号中,你可以将一个属性的键指定为这个表达式: ```js const FOO = 'foo'; const { [FOO]: f } = { foo: 123 }; // f = 123 ``` 可计算的属性键允许解构键为 symbol 类型的属性: ```js // 创建并解构一个属性,属性的键是一个 symbol const KEY = Symbol(); const obj = { [KEY]: 'abc' }; const { [KEY]: x } = obj; // x = 'abc' // 提取 Array.prototype[Symbol.iterator] const { [Symbol.iterator]: func } = []; console.log(typeof func); // function ``` ## 10.7 数组解构的更多特性 ### 10.7.1 省略(elision) 省略(elision)允许在解构时使用数组的“空洞”来跳过不关心的元素: ```js const [,, x, y] = ['a', 'b', 'c', 'd']; // x = 'c'; y = 'd' ``` ### 10.7.2 10.7.2 剩余操作符(rest operator, ...) 剩余操作符(rest operator) 允许将数组的剩余元素提取到一个数组中。你只能把剩余操作符当作数组模式的最后一部分来使用: ```js const [x, ...y] = ['a', 'b', 'c']; // x='a'; y=['b', 'c'] ``` > [展开操作符](###)(spread operator)具有与剩余运算符完全相同的语法 - `...`。但它们是不同的:前者向对象字面量和函数调用提供数据,而后者则用于解构和提取数据。 如果剩余操作符找不到任何元素,就会将运算元(operand)匹配到空数组。也就是说,它不会产生 `undefined` 或者 `null`。比如: ```js const [x, y, ...z] = ['a']; // x='a'; y=undefined; z=[] ``` 剩余操作符的运算元不一定是变量,还可以是模式: ```js const [x, ...[y, z]] = ['a', 'b', 'c']; // x = 'a'; y = 'b'; z = 'c' ``` 剩余操作符将触发以下解构: ```js [y, z] = ['b', 'c'] ``` ## 10.8 不止可以给变量赋值 使用解构赋值时,每个赋值目标可以是一个正常赋值的左侧所允许的任何内容。 例如,对`property(obj.prop)`的引用: ```js const obj = {}; ({ foo: obj.prop } = { foo: 123 }); console.log(obj); // {prop:123} ``` 或者引用一个数组元素(`arr[0]`): ```js const arr = []; ({ bar: arr[0] } = { bar: true }); console.log(arr); // [true] ``` 您还可以通过剩余操作符`(...)`将对象属性和数组元素分配: ```js const obj = {}; [first, ...obj.prop] = ['a', 'b', 'c']; // first = 'a'; obj.prop = ['b', 'c'] ``` 假如你通过解构来声明变量或者定义参数,必须使用简单标识符,而不能是对象属性和数组元素的引用。 ## 10.9 解构的陷阱 在使用解构时要注意以下两点: 1. 声明不要以花括号开始。 2. 解构期间,要么声明变量,要么给变量赋值,但是不能同时进行。 下面详细解释。 ### 10.9.1 声明不要以花括号开始。 因为代码块是以花括号开始的,所以声明不能这样。 当在赋值操作中使用对象解构时,很不巧,会出现这种情况: ```js { a, b } = someObject; // SyntaxError ``` 解决办法是给整个表达式加上圆括号: ```js ({ a, b } = someObject); // OK ``` 下面的是错误示例: ```js ({ a, b }) = someObject; // SyntaxError ``` 如果前面带上 `let`, `var` 和 `const` 的话,则可以放心使用花括号: ```js const { a, b } = someObject; // OK ``` ### 10.9.2 不要同时进行声明和对已有变量的赋值操作 在解构变量声明中,解构源的每个变量都会被声明。下面的例子里,我们试图声明变量 b,以及引用变量 f,然而并不会成功。 ```js let f; ··· let { foo: f, bar: b } = someObject; // 解析阶段(在运行代码之前): // SyntaxError: Duplicate declaration, f ``` 修复的方法是在解构中只进行赋值操作,并且预先声明变量 b: ```js let f; ··· let b; ({ foo: f, bar: b } = someObject); ``` ## 10.10 解构的例子 先看几个小例子。 for-of 循环支持解构: ```js const map = new Map().set(false, 'no').set(true, 'yes'); for (const [key, value] of map) { console.log(key + ' is ' + value); } ``` 你可以用解构来交换值。JavaScript 引擎会优化这个操作,所以不会额外创建数组。 ```js [a, b] = [b, a]; ``` 你还可以用解构来切分数组: ```js const [first, ...rest] = ['a', 'b', 'c']; // first = 'a'; rest = ['b', 'c'] ``` ### 10.10.1 解构返回的数组 一些内置的 JavaScript 操作会返回数组。解构能帮忙处理它们: ```js const [all, year, month, day] = /^(\d\d\d\d)-(\d\d)-(\d\d)$/ .exec('2999-12-31'); ``` 假如你只想得到正则里的分组(而不是匹配的整体, all),你可以使用省略,略过下标为 0 的数组元素: ```js const [, year, month, day] = /^(\d\d\d\d)-(\d\d)-(\d\d)$/ .exec('2999-12-31'); ``` 假如正则表达式不能成功匹配,`exec()` 会返回 `null`。遗憾的是,由于返回 `null`无法给变量设置默认值,所以此时需要用或操作符(||): ```js const [, year, month, day] = /^(\d\d\d\d)-(\d\d)-(\d\d)$/ .exec(someStr) || []; ``` `Array.prototype.split()` 会返回一个数组。因此,假如你只关心元素而不关心数组的话,可以用解构: ```js const cells = 'Jane\tDoe\tCTO' const [firstName, lastName, title] = cells.split('\t'); console.log(firstName, lastName, title); ``` ### 10.10.2 解构返回的对象 解构可用于从函数或者方法返回的对象中提取数据。比如,迭代器方法 `next()` 返回一个对象,该对象有两个属性, done 和 value。下面的代码通过迭代器 iter 打印出数组 arr 的所有元素。行 `A` 使用了解构: ```js const arr = ['a', 'b']; const iter = arr[Symbol.iterator](); while (true) { const {done,value} = iter.next(); // (A) if (done) break; console.log(value); } ``` ### 10.10.3 数组解构(array-destructuring)可迭代的值 数组解构可以作用于任何可迭代的值。有时候会比较有用: ```js const [x,y] = new Set().add('a').add('b'); // x = 'a'; y = 'b' const [a,b] = 'foo'; // a = 'f'; b = 'o' ``` ### 10.10.4 多重返回值 为了证明多重返回值的好处,我们实现一个函数 findElement(a, p),这个函数用于查找数组 a 中,第一个使得函数 p 返回 true 的元素。问题来了:函数 findElement(a, p) 应该返回什么?有时候我们只需要返回元素本身,有时候只需要其下标,有时候两者都需要。下面的实现返回了两者。 ```js function findElement(array, predicate) { for (const [index, element] of array.entries()) { // (A) if (predicate(element)) { return { element, index }; // (B) } } return { element: undefined, index: -1 }; } ``` 在行 A 中,数组方法 entries() 返回一个可迭代的 `[index,element]` 对。每次迭代将解构一个`[index,element]` 对。在行 B 中,我们使用属性值缩写返回了对象 `{ element: element, index: index }`。 接下来使用 `findElement()`。 ```js const arr = [7, 8, 6]; const {element, index} = findElement(arr, x => x % 2 === 0); // element = 8, index = 1 ``` > 有几个 ECMAScript 6 的功能让我们可以写更多的简洁的代码:回调函数是一个箭头函数,返回值是从一个属性值缩写的对象模式中解构出来的。 由于 `index` 和 `element` 都指向属性名,所以可以不分先后顺序: ```js const {index, element} = findElement(···); ``` 以上例子满足了同时返回下标和元素的需求。假如我们只关心其中一个返回值呢?好在ECMAScript 6 有解构功能,上面的实现也可以满足单个返回值的需求。而且,跟单个返回值的函数相比,这种实现方式的句法开销是最小的。 ```js const a = [7, 8, 6]; const {element} = findElement(a, x => x % 2 === 0); // element = 8 const {index} = findElement(a, x => x % 2 === 0); // index = 1 ``` 我们每次只提取需要的属性值。 ## 10.11 解构的算法 这一节将从另一个角度审视解构:递归模式匹配算法。 > 这一角度特别有助于理解默认值。假如你觉得自己还没完全理解默认值,请接着看。 在这最后一节里,我会使用算法来解释下面两个函数声明的区别。 ```js function move({x=0, y=0} = {}) { ··· } function move({x, y} = { x: 0, y: 0 }) { ··· } ``` ### 10.11.1 算法 一个解构赋值看起来是这样的: ~~~ «pattern» = «value» ~~~ 我们要使用 `pattern` 从 `value` 中提取数据。我先描述一下实现这个功能的算法,在函数式编程中该算法叫做 模式匹配(简称:匹配)。该算法将一个操作符 ← (“匹配”)指定给解构赋值,这个解构赋值会用一个 `pattern` 去匹配一个 `value` ,同时赋值给变量: ~~~ «pattern» ← «value» ~~~ 该算法是通过一些迭代的规则来定义的,这些规则会分别解析 ← 操作符两边的运算元。你可能还不习惯这个声明符号,但是这个符号能让算法的定义更简洁。每个迭代规则由以下两部分构成: * 头部(head)指明了规则所操作的运算元。 * 主体部分(body)指定了下一步要执行的动作。 让我们来看一个例子: (2c) `{key: «pattern», «properties»}` ← `obj` ~~~ «pattern» ← obj.key {«properties»} ← obj ~~~ (2e) `{}` ← `obj` (no properties left) ~~~ // Nothing to do ~~~ 在规则(2c)中,头意味着,如果存在至少一个属性和零个或多个属性的对象模式,则执行该规则。该模式与`obj`匹配。此规则的作用是继续执行属性值模式与obj.key匹配,剩下的属性与obj匹配。 在规则(2e)中,头意味着如果空对象模式{}与值obj匹配,则执行该规则。 每当调用算法时,规则都会被从上到下的检查,并且只有第一个适用的规则被执行。 这里只展示解构赋值的算法。解构变量声明和解构参数定义的算法跟这个算法很相似。 我也不会介绍更高级的特性(计算属性键;属性值缩写;赋值目标的对象属性和数组元素)。这里只介绍基础知识。 #### 10.11.1.1 模式 一个模式可能是以下三种情况之一: * 一个变量:`x` * 一个对象模式: `{«properties»}` * 一个数组模式: `[«elements»]` 下面的小节里会分别介绍这三种情况。 #### 10.11.1.2 变量 (1) `x ← value` (包括 `undefined` 和 `null`) ```js x = value ``` #### 10.11.1.3 对象模式 * (2a) `{«properties»} ← undefined` ```js throw new TypeError(); ``` * (2b) `{«properties»} ← null` ```js throw new TypeError(); ``` * (2c) `{key: «pattern», «properties»} ← obj` ```js «pattern» ← obj.key {«properties»} ← obj ``` * (2d) `{key: «pattern» = default_value, «properties»} ← obj` ```js const tmp = obj.key; if (tmp !== undefined) { «pattern» ← tmp } else { «pattern» ← default_value } {«properties»} ← obj ``` * (2e) `{} ← obj` ``` // Nothing to do ``` #### 10.11.1.4 数组模式 **数组模式和可迭代值** 数组解构算法以数组模式和一个迭代器开始: * (3a) `[«elements»] ← non_iterable` `assert(!isIterable(non_iterable))` ```js throw new TypeError(); ``` * (3b) `[«elements»] ← iterable` `assert(isIterable(iterable))` ```js const iterator = iterable[Symbol.iterator](); «elements» ← iterator ``` 辅助函数(Helper function:): ```js function isIterable(value) { return (value !== null && typeof value === 'object' && typeof value[Symbol.iterator] === 'function'); } ``` **数组元素和迭代器**。 接下来,算法要处理模式里的元素(箭头左侧)以及从可迭代值里得到的迭代器(箭头右侧)。 * (3c) `«pattern», «elements» ← iterator` ~~~ «pattern» ← getNext(iterator) // 最后一个元素之后就是 undefined «elements» ← iterator ~~~ * (3d) `«pattern» = default_value, «elements» ← iterator` ~~~ const tmp = getNext(iterator); // 最后一个元素之后就是 undefined if (tmp !== undefined) { «pattern» ← tmp } else { «pattern» ← default_value } «elements» ← iterator ~~~ * (3e) `, «elements» ← iterator `(“空洞”, 省略) ~~~ getNext(iterator); // 略过 «elements» ← iterator ~~~ * (3f) `...«pattern» ← iterator` (一定是数组最后一部分!) ~~~ const tmp = []; for (const elem of iterator) { tmp.push(elem); } «pattern» ← tmp ~~~ * (3g) ← iterator ~~~ // 没有元素了,什么也不做 ~~~ ### 10.11.2 应用算法 在ECMAScript 6中,如果调用者使用一个对象字面量,并且被调用者使用解构,您可以模拟命名的参数(named parameters)。这个模拟在[参数处理的章节](###)中有详细的解释。 ```js function move1({x=0, y=0} = {}) { // (A) return [x, y]; } move1({x: 3, y: 8}); // [3, 8] move1({x: 3}); // [3, 0] move1({}); // [0, 0] move1(); // [0, 0] ``` 在行A中有三个默认值: - 前两个默认值允许您省略`x`和`y`。 - 第三个默认值允许您不带参数调用`move1()`(类似在最后一行)。 可为什么非要像上面的代码那样定义参数呢?为什么不是下面这种方式?下面的代码也是完全合法的 ES6 代码呀。 ```js function move2({x, y} = { x: 0, y: 0 }) { return [x, y]; } ``` 要解释为什么 `move1()` 是正确的做法,我们可以通过在两个例子里使用这两种函数进行比较。不过在此之前,先看看匹配过程是如何解释参数传递的。 #### 10.11.2.1 背景:通过匹配传参 对于函数调用,实参(在函数调用里)会去匹配形参(在函数定义里)。举个例子,下面就是函数定义和函数调用。 ```js function func(a=0, b=0) { ··· } func(1, 2); ``` 参数 a 和 b 会按照类似于下面的解构进行赋值。 ```js [a=0, b=0] ← [1, 2] ``` #### 10.11.2.2 使用 `move2()` 让我们看看对于 `move2()`,解构是如何进行的。 **例 1:** `move2() `的解构过程如下: ```js [{x, y} = { x: 0, y: 0 }] ← [] ``` 左侧唯一的数组元素在右侧没有找到对应的匹配值,所以 {x,y} 匹配了默认值,而不是匹配右侧的数据(规则 3b, 3d): ```js {x, y} ← { x: 0, y: 0 } ``` 左侧包含了 属性值缩写,展开如下: ```js {x: x, y: y} ← { x: 0, y: 0 } ``` 解构进行了以下两个赋值操作(规则 2c,1): ```js x = 0; y = 0; ``` 不过,只有像 `move2()` 这样不传参数的函数调用才会用到默认值。 **例 2:** 函数调用 `move2({z:3})` 的解构过程如下: ```js [{x, y} = { x: 0, y: 0 }] ← [{z:3}] ``` 右侧数组有下标为 0 的元素。所以,会忽略默认值,下一步是(规则 3d): ```js {x, y} ← { z: 3 } ``` 这会把`x` 和 `y` 都设置成 `undefined`,这可不是我们想要的结果。 #### 10.11.2.3 使用 `move1()` 试试 `move1()`。 **例 1:** `move1()` ```js [{x=0, y=0} = {}] ← [] ``` 右侧没有下标为0的数组元素,所以使用默认值(规则 3d): ```js {x=0, y=0} ← {} ``` 左侧包含了 属性值缩写,展开如下: ```js {x: x=0, y: y=0} ← {} ``` x 和 y 在右侧都没有匹配值。因此,会使用默认值,于是会进行下面的解构(规则 2d): ```js x ← 0 y ← 0 ``` 接下来执行如下的赋值操作(规则 1): ```js x = 0 y = 0 ``` **例 2: **`move1({z:3})` ```js [{x=0, y=0} = {}] ← [{z:3}] ``` 数组模式的第一个元素在右侧有匹配值,使用该匹配值进行解构(规则 3d): ```js {x=0, y=0} ← {z:3} ``` 跟例 1 相似,右侧不存在属性 x 和 y ,因此使用默认值: ```js x = 0 y = 0 ``` #### 10.11.2.4 总结 以上例子展示了默认值属于模式部分(对象属性或者数组元素)的一个特性。假如模式的某一部分没有匹配值或者匹配了 undefined,那么就会使用默认值。换句话说,模式匹配了默认值。