[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,那么就会使用默认值。换句话说,模式匹配了默认值。
- 关于本书
- 目录简介
- 关于这本书你需要知道的
- 序
- 前言
- 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过时了吗?
- ==个人笔记==