10. 解构
目录
请支持本书:购买 (PDF, EPUB, MOBI)捐赠
(广告,请不要屏蔽。)

10. 解构



10.1 概述

解构是一种从存储在(可能是嵌套的)对象和数组中的数据中提取多个值的便捷方法。它可以在接收数据的位置使用(例如赋值的左侧)。如何提取值是通过模式指定的(请继续阅读示例)。

10.1.1 对象解构

解构对象

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

// {prop} is short for {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 数组解构

数组解构(适用于所有可迭代值)

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 解构可以在哪里使用?

解构可以在以下位置使用(我展示了数组模式来演示;对象模式同样有效)

// Variable declarations:
const [x] = ['a'];
let [x] = ['a'];
var [x] = ['a'];

// Assignments:
[x] = ['a'];

// Parameter definitions:
function f([x]) { ··· }
f(['a']);

你也可以在 for-of 循环中进行解构

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

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

为了充分理解解构是什么,让我们首先考察一下它的更广泛的背景。

JavaScript 具有用于构造数据的操作,一次一个属性

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

相同的语法可用于提取数据。同样,一次一个属性

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

此外,还有一种语法可以通过对象字面量同时构造多个属性

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

在 ES6 之前,没有相应的机制来提取数据。这就是解构的作用——它允许你通过对象模式从对象中提取多个属性。例如,在赋值的左侧

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

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

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

10.3 解构模式

以下两方参与解构

解构目标是以下三种模式之一

这意味着你可以任意深度地嵌套模式

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 对象模式将值强制转换为对象

对象模式在访问属性之前将解构源强制转换为对象。这意味着它适用于原始值

const {length : len} = 'abc'; // len = 3
const {toString: s} = 123; // s = Number.prototype.toString
10.4.1.1 无法对象解构值

强制转换为对象的执行不是通过 Object(),而是通过内部操作 ToObject()。这两个操作对 undefinednull 的处理方式不同。

Object() 将原始值转换为包装对象,并保持对象不变

> typeof Object('abc')
'object'

> var obj = {};
> Object(obj) === obj
true

它还将 undefinednull 转换为空对象

> Object(undefined)
{}
> Object(null)
{}

相反,如果 ToObject() 遇到 undefinednull,则会抛出 TypeError。因此,以下解构会失败,甚至在解构访问任何属性之前

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

因此,你可以使用空对象模式 {} 来检查值是否可以强制转换为对象。正如我们所见,只有 undefinednull 不能

({} = [true, false]); // OK, Arrays are coercible to objects
({} = 'abc'); // OK, strings are coercible to objects

({} = 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'

你不能通过索引访问集合的元素,但可以通过迭代器访问。因此,数组解构适用于集合

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

Set 迭代器总是按照插入元素的顺序返回元素,这就是为什么先前解构的结果总是相同的原因。

10.4.2.1 无法数组解构值

如果一个值有一个键为 Symbol.iterator 的方法,该方法返回一个对象,则该值是可迭代的。如果要解构的值不可迭代,则数组解构会抛出一个 TypeError

let x;
[x] = [true, false]; // OK, Arrays are iterable
[x] = 'abc'; // OK, strings are iterable
[x] = { * [Symbol.iterator]() { yield 1 } }; // OK, iterable

[x] = {}; // TypeError, empty objects are not iterable
[x] = undefined; // TypeError, not iterable
[x] = null; // TypeError, not iterable

即使在访问可迭代对象的元素之前也会抛出 TypeError,这意味着你可以使用空数组模式 [] 来检查值是否可迭代

[] = {}; // TypeError, empty objects are not iterable
[] = undefined; // TypeError, not iterable
[] = null; // TypeError, not iterable

10.5 默认值

默认值是模式的可选特性。如果在源中没有找到任何内容,它们将提供一个回退。如果一个部分(一个对象属性或一个数组元素)在源中没有匹配项,则将其与

让我们看一个例子。在下面的解构中,索引 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 undefined 触发默认值

如果一个部分确实有匹配项,并且该匹配项是 undefined,则也会使用默认值

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

这种行为的基本原理将在下一章参数默认值部分中解释。

10.5.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')] = [];
> a
'YES'

> const [b=log('hello')] = [123];
> b
123

在第二个解构中,默认值不会被触发,并且不会调用 log()

10.5.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.4 模式的默认值

到目前为止,我们只看到了变量的默认值,但你也可以将它们与模式相关联

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

这是什么意思?回想一下默认值的规则:如果一个部分在源中没有匹配项,则解构将继续使用默认值。

索引 0 处的元素没有匹配项,这就是为什么解构继续使用

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

如果你将模式 { prop: x } 替换为变量 pattern,你可以更容易地理解为什么会这样工作

const [pattern = {}] = [];

10.5.5 更复杂的默认值

让我们进一步探讨模式的默认值。在下面的例子中,我们通过默认值 { 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

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

您还可以将属性值简写与默认值组合使用

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

10.6.2 计算属性键

计算属性键是另一种对象字面量特性,它也适用于解构。如果将表达式放在方括号中,则可以通过表达式指定属性的键

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

计算属性键允许您解构键为符号的属性

// Create and destructure a property whose key is a symbol
const KEY = Symbol();
const obj = { [KEY]: 'abc' };
const { [KEY]: x } = obj; // x = 'abc'

// Extract Array.prototype[Symbol.iterator]
const { [Symbol.iterator]: func } = [];
console.log(typeof func); // function

10.7 更多数组解构特性

10.7.1 省略

省略允许您使用数组“空洞”的语法在解构期间跳过元素

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

10.7.2 剩余运算符 (...)

剩余运算符允许您将可迭代对象的剩余元素提取到数组中。如果此运算符在数组模式中使用,则它必须放在最后

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

如果运算符找不到任何元素,则将其操作数与空数组匹配。也就是说,它永远不会产生 undefinednull。例如

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)

const obj = {};
({ foo: obj.prop } = { foo: 123 });
console.log(obj); // {prop:123}

或者对数组元素的引用 (arr[0])

const arr = [];
({ bar: arr[0] } = { bar: true });
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.10 解构示例

让我们从一些较小的例子开始。

for-of 循环支持解构

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

您可以使用解构来交换值。这是引擎可以优化的事情,因此不会创建数组。

[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,这就是为什么在这种情况下必须使用 Or 运算符 (||) 的原因

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 数组解构可迭代值

数组解构适用于任何可迭代值。这偶尔会有用

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() 应该返回什么?有时一个人对元素本身感兴趣,有时对其索引感兴趣,有时对两者都感兴趣。以下实现返回两者。

function findElement(array, predicate) {
    for (const [index, element] of array.entries()) { // (A)
        if (predicate(element, index, array)) {
            // We found an element:
            return { element, index };
                // Same as (property value shorthands):
                // { element: element, index: index }
        }
    }
    // We couldn’t find anything; return failure values:
    return { element: undefined, index: -1 };
}

该函数通过数组方法 entries() 迭代数组 array 的所有元素,该方法返回 [index,element] 对上的可迭代对象(A 行)。对的部分通过解构访问。

让我们使用 findElement()

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

几个 ECMAScript 6 特性使我们能够编写更简洁的代码:回调是一个箭头函数;返回值通过具有属性值简写的对象模式进行解构。

由于 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 中提取数据。我现在将描述一种用于执行此操作的算法,该算法在函数式编程中称为模式匹配(简称:匹配)。该算法为解构赋值指定了运算符 (“匹配”),该运算符将 patternvalue 匹配,并在这样做时分配给变量

«pattern»  «value»

该算法是通过递归规则指定的,这些规则将 运算符的两个操作数分开。声明性符号可能需要一些时间来适应,但它使算法的规范更加简洁。每条规则都有两部分

让我们看一个例子

在规则 (2c) 中,头部表示如果存在至少具有一个属性和零个或多个剩余属性的对象模式,则执行此规则。该模式与值 obj 匹配。此规则的效果是执行继续,属性值模式与 obj.key 匹配,其余属性与 obj 匹配。

在规则 (2e) 中,头部表示如果空对象模式 {} 与值 obj 匹配,则执行此规则。然后就没什么可做的了。

每当调用算法时,都会从上到下检查规则,并且只执行适用的第一条规则。

我仅展示了解构赋值的算法。解构变量声明和解构参数定义的工作方式类似。

我也不涵盖高级特性(计算属性键;属性值简写;对象属性和数组元素作为赋值目标)。只有基础知识。

10.11.1.1 模式

模式是以下之一

以下各节描述了这三种情况之一。

以下三节指定了如何处理这三种情况。每节包含一个或多个编号规则。

10.11.1.2 变量
10.11.1.3 对象模式
10.11.1.4 数组模式

数组模式和可迭代对象。 数组解构的算法从数组模式和可迭代对象开始

辅助函数

function isIterable(value) {
    return (value !== null
        && typeof value === 'object'
        && typeof value[Symbol.iterator] === 'function');
}

数组元素和迭代器。 该算法继续使用模式的元素(箭头左侧)和从可迭代对象获得的迭代器(箭头右侧)。

辅助函数

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

10.11.2 应用算法

在 ECMAScript 6 中,如果调用者使用对象字面量而被调用者使用解构,则可以模拟命名参数。这种模拟在关于参数处理的章节中详细解释。以下代码显示了一个示例:函数 move1() 有两个命名参数,xy

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 行中有三个默认值

但是为什么要像前面的代码片段那样定义参数呢?为什么不像下面这样——这也是完全合法的 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()

示例 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;

示例 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}  {}

属性 x 和属性 y 在右侧都没有匹配项。因此,将使用默认值,并执行以下解构(规则 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.2.4 结论

这些示例表明,默认值是模式部分(对象属性或数组元素)的一个特性。如果某个部分没有匹配项或与 undefined 匹配,则使用默认值。也就是说,模式是与默认值匹配的。

下一页:11. 参数处理