huangxiaolu

第10章 解构(Destructuring) -- Exploring ES6

原文链接: exploringjs.com

目录

请多多支持本书:购买 (PDF, EPUB, MOBI)捐赠

10. 解构(Destructuring)

10.1 概览

解构(Destructuring) 是一种从数据中提取值的便捷方式,这些数据存储在(可能嵌套的)对象和数组中。解构可以用在接收数据的地方(比如赋值操作的左边)。提取的具体方式取决于模式(看后面的例子就明白啦)。

10.1.1 对象解构(Object destructuring)

解构对象:

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'

解构能帮助处理返回值:

const obj = { foo: 123 };

const {writable, configurable} =
    Object.getOwnPropertyDescriptor(obj, 'foo');

console.log(writable, configurable); // true true

10.1.2 数组解构(Array destructuring)

数组解构(作用于所有可迭代的值):

const iterable = ['a', 'b'];
const [x, y] = iterable;
    // x = 'a'; y = 'b'

解构能帮助处理返回值:

const [all, year, month, day] =
    /^(\d\d\d\d)-(\d\d)-(\d\d)$/
    .exec('2999-12-31');

10.1.3 解构可以用在什么地方?

解构可以用在以下地方:

// 变量声明:
const [x] = ['a'];
let [x] = ['a'];
var [x] = ['a'];

// 赋值:
[x] = ['a'];

// 参数定义:
function f([x]) { ··· }
f(['a']);

你还可以在for-of循环里进行解构:

const arr1 = ['a', 'b'];
for (const [index, element] of arr1.entries()) {
    console.log(index, element);
}
// 输出:
// 0 a
// 1 b

const arr2 = [
    {name: 'Jane', age: 41},
    {name: 'John', age: 40},
];
for (const {name, age} of arr2) {
    console.log(name, age);
}
// 输出:
// Jane 41
// John 40

10.2 背景:构造数据 vs 提取数据

为了充分理解什么是解构,我们先看看它所在的更广义的上下文环境。JavaScript 有以下操作来构造数据:

const obj = {};
obj.first = 'Jane';
obj.last = 'Doe';

并且有以下操作来提取数据:

const f = obj.first;
const l = obj.last;

注意,我们提取数据的时候用了跟构造数据时一样的语法。

还有更好的构造语法——对象字面量

const obj = { first: 'Jane', last: 'Doe' };

在 ECMAScript 6 里_解构允许使用同样的语法提取数据,这种语法在提取数据时叫做对象模式_:

const { first: f, last: l } = obj;

就像对象字面量允许同时创建多个属性一样,对象模式允许我们同时提取多个属性。

你也可以通过模式解构数组:

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» ]

    • 数组模式的组成部分是元素,元素还是模式(可递归)。

这意味着能够以任意的深度嵌套模式:

const obj = { a: [{ foo: 123, bar: 'abc' }, {}], b: true };
const { a: [{foo: f}] } = obj; // f = 123

10.3.1 按需挑选模式

假如你要解构一个对象,你只需要写你想要的属性:

const { x: x } = { x: 7, y: 3 }; // x = 7

假如你要解构一个数组,你可以选择只提取前面的部分:

const [x,y] = ['a', 'b', 'c']; // x='a'; y='b';

10.4 模式是如何访问值的内部结构的?

pattern = someValue 这个赋值里, pattern 是如何访问 someValue 内部的呢?

10.4.1 对象模式强制将值转化成对象处理

对象模式在访问属性之前会强制将解构源转化成对象。这意味着它能处理原始类型值(primitive values):

const {length : len} = 'abc'; // len = 3
const {toString: s} = 123; // s = Number.prototype.toString

10.4.1.1 有时候,无法对值进行对象解构

强制转化成对象的操作并不是通过 Object() 实现的,而是通过内部操作 ToObject()Object() 永远都不会失败:

> typeof Object('abc')
'object'
> var obj = {};
> Object(obj) === obj
true
> Object(undefined)
{}
> Object(null)
{}

当遇到 undefinednull 时, ToObject() 会抛一个 TypeError 错误。因此,下面的解构在访问任何属性之前就失败了:

const { prop: x } = undefined; // TypeError
const { prop: y } = null; // TypeError

所以,你可以使用空对象模式 {} 检查一个值能否强制转换成对象。我们已经知道,只有 undefinednull 不能:

({} = [true, false]); // OK,数组强制转换成对象
({} = 'abc'); // OK,字符串强制转换成对象

({} = undefined); // TypeError
({} = null); // TypeError

以上表达式外面的圆括号是必须的,因为在 JavaScript 里,声明不可以用花括号开始(细节稍后解释)。

10.4.2 数组模式对可迭代的值都可以生效

数组解构使用了迭代器来获取解构源的元素。因此,你可以数组解构任何可迭代的值。我们来看几个可迭代值的例子。

字符串是可迭代的:

const [x,...y] = 'abc'; // x='a'; y=['b', 'c']

别忘了,字符串的迭代器返回的是代码点(“Unicode 字符”, 21 位),而不是代码单元(“JavaScript 字符”, 16 位)。(更多关于 Unicode 的信息,参考“Speaking Javascript”这本书里的“第 24 章 Unicode 与 JavaScript”。) 比如:

const [x,y,z] = 'a\uD83D\uDCA9c'; // x='a'; y='\uD83D\uDCA9'; z='c'

你不能通过索引访问 Set 的元素,但是你可以通过迭代器来访问。因此,数组解构也支持 Set:

const [x,y] = new Set(['a', 'b']); // x='a'; y='b’;

Set 迭代器按照插入顺序返回元素,这也是解释了为什么上面的解构每次返回的结果都相同。

无穷序列。 解构也可用于无穷序列的迭代器。 生成器函数 allNaturalNumbers() 返回一个生成 0, 1, 2 … 的迭代器。

function* allNaturalNumbers() {
  for (let n = 0; ; n++) {
    yield n;
  }
}

下面的解构能提取上面无穷序列的前三个元素。

const [x, y, z] = allNaturalNumbers(); // x=0; y=1; z=2

10.4.2.1 有时候,无法对值进行数组解构

当一个值有 Symbol.iterator 方法,且这个方法能返回一个对象时,它就是可迭代的。假如被解构的值不可迭代,数组解构就会抛出 TypeError 的错误:

let x;
[x] = [true, false]; // OK,数组是可迭代的
[x] = 'abc'; // OK, 字符串是可迭代的
[x] = { * [Symbol.iterator]() { yield 1 } }; // OK,可迭代

[x] = {}; // TypeError,空对象不可迭代
[x] = undefined; // TypeError,不可迭代
[x] = null; // TypeError,不可迭代

访问数组元素之前,就会抛出 TypeError ,这意味着你可以用空数组模式 [] 检查一个值是不是可迭代的:

[x] = {}; // TypeError,空对象不可迭代
[x] = undefined; // TypeError,不可迭代
[x] = null; // TypeError,不可迭代

10.5 如果有一部分没有匹配到

与 JavaScript 处理不存在的属性和数组元素相似,当解构目标中有一部分不存在于解构源中时,解构就会默默失败:这一部分内部会匹配到 undefined。如果内部是一个变量,意味着这个变量的值会被设置为 undefined

const [x] = []; // x = undefined
const {prop:y} = {}; // y = undefined

注意,假如对象模式和数组模式匹配到 undefined 时,会抛一个 TypeError

10.5.1 默认值(Default values)

默认值(Default values) 是模式的一个特性: 假如有一部分(一个对象属性或者一个数组元素)在解构源中没有匹配到,那么它就会被匹配成:

  • 它的 默认值 (如果指定了的话)

  • undefined (没指定时)

也就是说,提供一个默认值是可选的操作。

来看一个例子。在下面的解构中,下标为 0 的元素在右边没有匹配值。因此,解构会继续将 x 匹配成 3,结果就是 x 被设置成了 3。

const [x=3, y] = []; // x = 3; y = undefined

你也可以在对象模式中使用默认值:

const {foo: x=3, bar: y} = {}; // x = 3; y = undefined

10.5.1.1 undefined 会触发默认值

当某一部分有匹配值,并且匹配值是 undefined 时,也会最终匹配到默认值:

const [x=1] = [undefined]; // x = 1
const {prop: y=2} = {prop: undefined}; // y = 2

这一行为的根本原因将会在下一章的参数默认值一节中解释。

10.5.1.2 默认值是按需计算的

默认值本身只在需要时(即被触发时)进行计算。也就是说,下面的解构:

const {prop: y=someFunc()} = someValue;

等价于:

let y;
if (someValue.prop === undefined) {
    y = someFunc();
} else {
    y = someValue.prop;
}

假如你用 console.log()的话,可以观察到这一点:

> 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 默认值可以指向模式中的其它变量

默认值可以指向任何变量,包括同一模式下的其它变量:

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

但是,要注意顺序:变量 xy 是从左到右声明的,假如在变量声明之前访问它,就会产生 ReferenceError

const [x=y, y=3] = []; // ReferenceError

10.5.1.4 模式的默认值

目前我们只看到了变量的默认值,其实也可以给模式设定默认值:

const [{ prop: x } = {}] = [];

这样做的意义是什么呢?我们来回顾一下默认值的规则:

假如在解构源中没有匹配到,解构会继续匹配默认值[…]。

因为匹配不到下标为 0 的元素,解构就会继续下面的匹配:

const { prop: x } = {}; // x = undefined

如果把模式 { prop: x} 替换成变量 pattern,就更容易理解了:

const [pattern = {}] = [];

更复杂的默认值。我们进一步探索模式的默认值。在下面的例子里,通过默认值 { prop: 123 }x 赋值:

const [{ prop: x } = { prop: 123 }] = [];

因为下标为 0 的数组元素在右侧没有匹配,解构就会继续下面的匹配,最终 x 被设为 123。

const { prop: x } = { prop: 123 };  // x = 123

但是,在下面这种情况下,即使右侧的默认值里有下标为 0 的元素,x 也不会被赋值。因为这个默认值不会被触发。

const [{ prop: x } = { prop: 123 }] = [{}];

在这种情况下,解构会继续下面的匹配:

const { prop: x } = {}; // x = undefined

因此,假如你希望无论是对象还是属性缺失,x 都默认为 123 的话,你需要给 x 本身指定一个默认值:

const [{ prop: x=123 } = {}] = [{}];

这样的话,解构就会像下面这样继续进行,无论右侧是 [{}] 还是 []

const { prop: x=123 } = {}; // x = 123

还有疑问?

稍后会有一节 从另一个角度——算法角度——来解释解构。也许能让你有新的见解。

10.6 对象解构的更多特性

10.6.1 属性值缩写

属性值缩写是对象字面量的一个特性:假如属性值用变量表示,且变量与属性的键同名,你就可以省略键。对于解构,也同样适用:

const { x, y } = { x: 11, y: 8 }; // x = 11; y = 8

上述声明等价于:

const { x: x, y: y } = { x: 11, y: 8 };

你也可以将默认值与属性值缩写结合起来:

const { x, y = 1 } = {}; // x = undefined; y = 1

10.6.2 可计算的属性键(Computed property keys)

可计算的属性键是对象字面量的另一个特点,这也同样适用于解构。通过将表达式放进方括号中,你可以将一个属性的键指定为这个表达式:

const FOO = 'foo';
const { [FOO]: f } = { foo: 123 }; // f = 123

可计算的属性键允许解构键为 symbol 类型的属性:

// 创建并解构一个属性,属性的键是一个 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)允许在解构时使用数组的“空洞”来跳过不关心的元素:

const [,, x, y] = ['a', 'b', 'c', 'd']; // x = 'c'; y = 'd'

10.7.2 剩余操作符(rest operator, ...

剩余操作符(rest operator) 允许将数组的剩余元素提取到一个数组中。你只能把剩余操作符当作数组模式的最后一部分来使用:

const [x, ...y] = ['a', 'b', 'c']; // x='a'; y=['b', 'c']

剩余操作符用于提取数据。展开操作符(spread operator)也用了同样的语法(...)。展开操作符向对象字面量和函数调用提供数据,在下一章会详细展开。

如果剩余操作符找不到任何元素,就会将运算元(operand)匹配到空数组。也就是说,它不会产生 undefined 或者 null。比如:

const [x, y, ...z] = ['a']; // x='a'; y=undefined; z=[]

剩余操作符的运算元不一定是变量,还可以是模式:

const [x, ...[y, z]] = ['a', 'b', 'c'];
    // x = 'a'; y = 'b'; z = 'c'

剩余操作符将触发以下解构:

[y, z] = ['b', 'c']

展开操作符 (...) 跟剩余操作符长得一模一样,但它用在函数调用和数组字面量中(而不是解构模式中)。

10.8 不止可以给变量赋值

使用解构赋值时,每个赋值目标可以是一个正常赋值的左侧所允许的任何内容,包括对属性的引用(obj.prop)和对数组元素的引用(arr[0])。

const obj = {};
const arr = [];

({ foo: obj.prop, bar: arr[0] } = { foo: 123, bar: true });

console.log(obj); // {prop:123}
console.log(arr); // [true]

你还可以用剩余操作符(...)给对象的属性和数组元素赋值:

const obj = {};
[first, ...obj.prop] = ['a', 'b', 'c'];
    // first = 'a'; obj.prop = ['b', 'c']

假如你通过解构来_声明_变量或者定义参数,必须使用简单标识符,而不能是对象属性和数组元素的引用。

10.9 解构的陷阱

在使用解构时要注意以下两点:

  • 声明不要以花括号开始。

  • 解构期间,要么声明变量,要么给变量赋值,但是不能同时进行。

下面详细解释。

10.9.1 声明不要以花括号开始。

因为代码块是以花括号开始的,所以声明不能这样。当在赋值操作中使用对象解构时,很不巧,会出现这种情况:

{ a, b } = someObject; // SyntaxError

解决办法是给整个表达式加上圆括号:

({ a, b } = someObject); // OK

下面的语法并不管用:

({ a, b }) = someObject; // SyntaxError

如果前面带上 letvarconst 的话,则可以放心使用花括号:

const { a, b } = someObject; // OK

10.9.2 不要同时进行声明和对已有变量的赋值操作

在解构变量声明中,解构源的每个变量都会被声明。下面的例子里,我们试图声明变量 b,以及引用变量 f,然而并不会成功。

let f;
···
let { foo: f, bar: b } = someObject;
    // 解析阶段(在运行代码之前):
    // SyntaxError: Duplicate declaration, f

修复的方法是在解构中只进行赋值操作,并且预先声明变量 b

let f;
···
let b;
({ foo: f, bar: b } = someObject);

10.10 解构的例子

先看几个小例子。

for-of 循环支持解构:

const map = new Map().set(false, 'no').set(true, 'yes');
for (const [key, value] of map) {
  console.log(key + ' is ' + value);
}

你可以用解构来交换值。JavaScript 引擎会优化这个操作,所以不会额外创建数组。

[a, b] = [b, a];

你还可以用解构来切分数组:

const [first, ...rest] = ['a', 'b', 'c'];
    // first = 'a'; rest = ['b', 'c']

10.10.1 解构返回的数组

一些内置的 JavaScript 操作会返回数组。解构能帮忙处理它们:

const [all, year, month, day] =
    /^(\d\d\d\d)-(\d\d)-(\d\d)$/
    .exec('2999-12-31');

假如你只想得到正则里的分组(而不是匹配的整体, all),你可以使用省略,略过下标为 0 的数组元素:

const [, year, month, day] =
    /^(\d\d\d\d)-(\d\d)-(\d\d)$/
    .exec('2999-12-31');

假如正则表达式不能成功匹配,exec() 会返回 null。遗憾的是,由于返回 null无法给变量设置默认值,所以此时需要用或操作符(||):

const [, year, month, day] =
    /^(\d\d\d\d)-(\d\d)-(\d\d)$/
    .exec(someStr) || [];

Array.prototype.split() 会返回一个数组。因此,假如你只关心元素而不关心数组的话,可以用解构:

const cells = 'Jane\tDoe\tCTO'
const [firstName, lastName, title] = cells.split('\t');
console.log(firstName, lastName, title);

10.10.2 解构返回的对象

解构可用于从函数或者方法返回的对象中提取数据。比如,迭代器方法 next() 返回一个对象,该对象有两个属性, donevalue。下面的代码通过迭代器 iter 打印出数组 arr 的所有元素。行 A 使用了解构:

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)可迭代的值

数组解构可以作用于任何可迭代的值。有时候会比较有用:

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) 应该返回什么?有时候我们只需要返回元素本身,有时候只需要其下标,有时候两者都需要。下面的实现返回了两者。

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()。下面的例子里,有几个 ECMAScript 6 的功能让我们可以写更多的简洁的代码:回调函数是一个箭头函数,返回值是从一个属性值缩写的对象模式中解构出来的。

const arr = [7, 8, 6];
const {element, index} = findElement(arr, x => x % 2 === 0);
    // element = 8, index = 1

由于 indexelement 都指向属性名,所以可以不分先后顺序:

const {index, element} = findElement(···);

以上例子满足了同时返回下标和元素的需求。假如我们只关心其中一个返回值呢?好在ECMAScript 6 有解构功能,上面的实现也可以满足单个返回值的需求。而且,跟单个返回值的函数相比,这种实现方式的句法开销是最小的。

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 解构的算法

这一节将从另一个角度审视解构:递归模式匹配算法。

这一角度特别有助于理解默认值。假如你觉得自己还没完全理解默认值,请接着看。

在这最后一节里,我会使用算法来解释下面两个函数声明的区别。

function move({x=0, y=0} = {})         { ··· }
function move({x, y} = { x: 0, y: 0 }) { ··· }

10.11.1 算法

一个解构赋值看起来是这样的:

«pattern» = «value»

我们要使用 patternvalue 中提取数据。我先描述一下实现这个功能的算法,在函数式编程中该算法叫做 模式匹配(简称:_匹配_)。该算法将一个操作符 (“匹配”)指定给解构赋值,这个解构赋值会用一个 pattern 去匹配一个 value ,同时赋值给变量:

«pattern» ← «value»

该算法是通过一些迭代的规则来定义的,这些规则会分别解析 操作符两边的运算元。你可能还不习惯这个声明符号,但是这个符号能让算法的定义更简洁。每个迭代规则由以下两部分构成:

  • 头部(head)指明了规则所操作的运算元。

  • 主体部分(body)指定了下一步要执行的动作。

这里只展示解构赋值的算法。解构变量声明和解构参数定义的算法跟这个算法很相似。

我也不会介绍更高级的特性(计算属性键;属性值缩写;赋值目标的对象属性和数组元素)。这里只介绍基础知识。

10.11.1.1 模式

一个模式可能是以下三种情况之一:

  • 一个变量: x

  • 一个对象模式: {«properties»}

  • 一个数组模式: [«elements»]

下面的小节里会分别介绍这三种情况。

10.11.1.2 变量
  • (1) x ← value (包括 undefinednull

      x = value
    
    
10.11.1.3 对象模式
  • (2a) {«properties»} ← undefined

      throw new TypeError();
    
    
  • (2b) {«properties»} ← null

      throw new TypeError();
    
    
  • (2c) {key: «pattern», «properties»} ← obj

      «pattern» ← obj.key
      {«properties»} ← obj
    
    
  • (2d) {key: «pattern» = default_value, «properties»} ← obj

      const tmp = obj.key;
      if (tmp !== undefined) {
          «pattern» ← tmp
      } else {
          «pattern» ← default_value
      }
      {«properties»} ← obj
    
    
  • (2e) {} ← obj

      // 模式中没有属性了,什么也不做
    
    
10.11.1.4 数组模式

数组模式和可迭代值 数组解构的算法以数组模式和可迭代值开始:

  • (3a) [«elements»] ← non_iterable

    assert(!isIterable(non_iterable))

      throw new TypeError();
    
    
  • (3b) [«elements»] ← iterable

    assert(isIterable(iterable))

      const iterator = iterable[Symbol.iterator]();
      «elements» ← iterator
    
    

帮助函数:

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

      // 没有元素了,什么也不做
    
    

帮助函数:

function getNext(iterator) {
    const {done,value} = iterator.next();
    return (done ? undefined : value);
}

10.11.2 解构算法的应用

下面的函数定义里使用了命名的参数(named parameters),这种技术有时候也叫选项对象(options object),在参数处理这一章有详细解释。参数使用了解构和默认值,这样一来,xy 可以省略不传。不仅如此,连参数里的对象都可以省略,比如下面代码的最后一行。这一特性通过第一行函数定义中的 = {} 实现。

function move1({x=0, y=0} = {}) {
    return [x, y];
}
move1({x: 3, y: 8}); // [3, 8]
move1({x: 3}); // [3, 0]
move1({}); // [0, 0]
move1(); // [0, 0]

可为什么非要像上面的代码那样定义参数呢?为什么不是下面这种方式?下面的代码也是完全合法的 ES6 代码呀。

function move2({x, y} = { x: 0, y: 0 }) {
    return [x, y];
}

要解释为什么 move1() 是正确的做法,我们可以通过在两个例子里使用这两种函数进行比较。不过在此之前,先看看匹配过程是如何解释参数传递的。

10.11.2.1 背景:通过匹配传参

对于函数调用,实参(在函数调用里)会去匹配形参(在函数定义里)。举个例子,下面就是函数定义和函数调用。

function func(a=0, b=0) { ··· }
func(1, 2);

参数 ab 会按照类似于下面的解构进行赋值。

[a=0, b=0] ← [1, 2]

10.11.2.2 使用 move2()

看看对于 move2(),解构是如何进行的。

Example 1. move2() 的解构过程如下:

[{x, y} = { x: 0, y: 0 }] ← []

左侧唯一的数组元素在右侧没有找到对应的匹配值,所以 {x,y} 匹配了默认值,而不是匹配右侧的数据(规则 3b, 3d):

{x, y} ← { x: 0, y: 0 }

左侧包含了 属性值缩写,展开如下:

{x: x, y: y} ← { x: 0, y: 0 }

解构进行了以下两个赋值操作(规则 2c,1):

x = 0;
y = 0;

不过,只有像 move2() 这样不传参数的函数调用才会用到默认值。

Example 2. 函数调用 move2({z:3}) 的解构过程如下:

[{x, y} = { x: 0, y: 0 }] ← [{z:3}]

右侧数组有下标为 0 的元素。所以,会忽略默认值,下一步是(规则 3d):

{x, y} ← { z: 3 }

这会把 xy 都设置成 undefined,这可不是我们想要的结果。

10.11.2.3 使用 move1()

试试 move1()

例 1: move1()

[{x=0, y=0} = {}] ← []

右侧没有下标为0的数组元素,所以使用默认值(规则 3d):

{x=0, y=0} ← {}

左侧包含了 属性值缩写,展开如下:

{x: x=0, y: y=0} ← {}

xy 在右侧都没有匹配值。因此,会使用默认值,于是会进行下面的解构(规则 2d):

x ← 0
y ← 0

接下来执行如下的赋值操作(规则 1):

x = 0
y = 0

例 2: move1({z:3})

[{x=0, y=0} = {}] ← [{z:3}]

数组模式的第一个元素在右侧有匹配值,使用该匹配值进行解构(规则 3d):

{x=0, y=0} ← {z:3}

跟例 1 相似,右侧不存在属性 xy ,因此使用默认值:

x = 0
y = 0

10.11.3 结论

以上例子展示了默认值属于模式部分(对象属性或者数组元素)的一个特性。假如模式的某一部分没有匹配值或者匹配了 undefined,那么就会使用默认值。换句话说,模式匹配了默认值。

下一章: 11. 参数处理