面向急切程序员的 JavaScript(ES2022 版)
请支持本书:购买捐赠
(广告,请勿屏蔽。)

37 解构



37.1 初探解构

使用普通的赋值,您可以一次提取一条数据——例如

const arr = ['a', 'b', 'c'];
const x = arr[0]; // extract
const y = arr[1]; // extract

使用解构,您可以通过接收数据的模式一次提取多条数据。 上述代码中 `=` 的左侧就是这样一个位置。 在下面的代码中,A 行中的方括号是一个解构模式

const arr = ['a', 'b', 'c'];
const [x, y] = arr; // (A)
assert.equal(x, 'a');
assert.equal(y, 'b');

这段代码与前面的代码作用相同。

请注意,模式比数据“小”:我们只提取我们需要的内容。

37.2 构造与提取

为了理解什么是解构,请考虑 JavaScript 有两种相反的操作

构造数据如下所示

// Constructing: one property at a time
const jane1 = {};
jane1.first = 'Jane';
jane1.last = 'Doe';

// Constructing: multiple properties
const jane2 = {
  first: 'Jane',
  last: 'Doe',
};

assert.deepEqual(jane1, jane2);

提取数据如下所示

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

// Extracting: one property at a time
const f1 = jane.first;
const l1 = jane.last;
assert.equal(f1, 'Jane');
assert.equal(l1, 'Doe');

// Extracting: multiple properties (NEW!)
const {first: f2, last: l2} = jane; // (A)
assert.equal(f2, 'Jane');
assert.equal(l2, 'Doe');

A 行中的操作是新的:我们声明了两个变量 `f2` 和 `l2`,并通过*解构*(多值提取)初始化它们。

A 行的以下部分是*解构模式*

{first: f2, last: l2}

解构模式在语法上类似于用于多值构造的字面量。 但它们出现在接收数据的地方(例如,赋值的左侧),而不是创建数据的地方(例如,赋值的右侧)。

37.3 解构的适用场景

解构模式可以在“数据接收位置”使用,例如

请注意,变量声明包括 `for-of` 循环中的 `const` 和 `let` 声明

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

接下来的两节中,我们将更深入地研究两种解构:对象解构和数组解构。

37.4 对象解构

*对象解构*允许您通过类似于对象字面量的模式批量提取属性值

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`。

您还可以对原始值进行对象解构

const {length: len} = 'abc';
assert.equal(len, 3);

您还可以对数组进行对象解构

const {0:x, 2:y} = ['a', 'b', 'c'];
assert.equal(x, 'a');
assert.equal(y, 'c');

为什么这样做有效? 数组索引也是属性

37.4.1 属性值简写

对象字面量支持属性值简写,对象模式也支持

const { street, city } = address;
assert.equal(street, 'Evergreen Terrace');
assert.equal(city, 'Springfield');

  练习:对象解构

exercises/destructuring/object_destructuring_exrc.mjs

37.4.2 剩余属性

在对象字面量中,您可以使用扩展属性。 在对象模式中,您可以使用剩余属性(必须放在最后)

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});

剩余属性变量(例如 `remaining`(A 行))被分配了一个对象,该对象包含所有数据属性,其键未在模式中提及。

`remaining` 也可以被视为从 `obj` 中非破坏性地删除属性 `a` 的结果。

37.4.3 语法陷阱:通过对象解构赋值

如果我们在赋值中进行对象解构,我们会遇到由 语法歧义 引起的陷阱——您不能用花括号开始语句,因为那样 JavaScript 会认为您要开始一个代码块

let prop;
assert.throws(
  () => eval("{prop} = { prop: 'hello' };"),
  {
    name: 'SyntaxError',
    message: "Unexpected token '='",
  });

  为什么使用 `eval()`?

`eval()` 会延迟解析(因此也会延迟 `SyntaxError`),直到执行 `assert.throws()` 的回调。 如果我们不使用它,我们在解析此代码时就已经会收到错误,并且 `assert.throws()` 甚至不会被执行。

解决方法是将整个赋值放在括号中

let prop;
({prop} = { prop: 'hello' });
assert.equal(prop, 'hello');

37.5 数组解构

*数组解构*允许您通过类似于数组字面量的模式批量提取数组元素的值

const [x, y] = ['a', 'b'];
assert.equal(x, 'a');
assert.equal(y, 'b');

您可以通过在数组模式中提及空位来跳过元素

const [, x, y] = ['a', 'b', 'c']; // (A)
assert.equal(x, 'b');
assert.equal(y, 'c');

A 行中数组模式的第一个元素是一个空位,这就是为什么数组中索引 0 处的元素被忽略的原因。

37.5.1 数组解构适用于任何可迭代对象

数组解构可以应用于任何可迭代的值,而不仅仅是数组

// Sets are iterable
const mySet = new Set().add('a').add('b').add('c');
const [first, second] = mySet;
assert.equal(first, 'a');
assert.equal(second, 'b');

// Strings are iterable
const [a, b] = 'xyz';
assert.equal(a, 'x');
assert.equal(b, 'y');

37.5.2 剩余元素

在数组字面量中,您可以使用扩展元素。 在数组模式中,您可以使用剩余元素(必须放在最后)

const [x, y, ...remaining] = ['a', 'b', 'c', 'd']; // (A)

assert.equal(x, 'a');
assert.equal(y, 'b');
assert.deepEqual(remaining, ['c', 'd']);

剩余元素变量(例如 `remaining`(A 行))被分配了一个数组,该数组包含解构值中尚未提及的所有元素。

37.6 解构示例

37.6.1 数组解构:交换变量值

您可以使用数组解构来交换两个变量的值,而无需使用临时变量

let x = 'a';
let y = 'b';

[x,y] = [y,x]; // swap

assert.equal(x, 'b');
assert.equal(y, 'a');

37.6.2 数组解构:返回数组的操作

当操作返回数组时,数组解构很有用,例如正则表达式方法 `.exec()`

// 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');

37.6.3 对象解构:多个返回值

如果函数返回多个值——打包为数组或打包为对象——则解构非常有用。

考虑一个在数组中查找元素的函数 `findElement()`

findElement(array, (value, index) => «boolean expression»)

它的第二个参数是一个函数,该函数接收元素的值和索引,并返回一个布尔值,指示这是否是调用者正在寻找的元素。

我们现在面临一个困境:`findElement()` 应该返回找到的元素的值还是索引? 一种解决方案是创建两个单独的函数,但这会导致代码重复,因为这两个函数非常相似。

以下实现通过返回一个包含找到的元素的索引和值的*对象*来避免重复

function findElement(arr, predicate) {
  for (let index=0; index < arr.length; index++) {
    const value = arr[index];
    if (predicate(value)) {
      // We found something:
      return { value, index };
    }
  }
  // We didn’t find anything:
  return { value: undefined, index: -1 };
}

解构帮助我们处理 `findElement()` 的结果

const arr = [7, 8, 6];

const {value, index} = findElement(arr, x => x % 2 === 0);
assert.equal(value, 8);
assert.equal(index, 1);

当我们使用属性键时,我们提及 `value` 和 `index` 的顺序无关紧要

const {index, value} = findElement(arr, x => x % 2 === 0);

关键是,如果我们只对两个结果中的一个感兴趣,解构也能很好地为我们服务

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);

所有这些便利加在一起,使得这种处理多个返回值的方式非常通用。

37.7 如果模式的一部分不匹配会发生什么?

如果模式的一部分没有匹配项会发生什么? 与使用非批量运算符时发生的情况相同:您将获得 `undefined`。

37.7.1 对象解构和缺少的属性

如果对象模式中的属性在右侧没有匹配项,您将获得 `undefined`

const {prop: p} = {};
assert.equal(p, undefined);

37.7.2 数组解构和缺少的元素

如果数组模式中的元素在右侧没有匹配项,您将获得 `undefined`

const [x] = [];
assert.equal(x, undefined);

37.8 哪些值不能解构?

37.8.1 不能对 `undefined` 和 `null` 进行对象解构

仅当要解构的值为 `undefined` 或 `null` 时,对象解构才会失败。 也就是说,只要通过点运算符访问属性会失败,它就会失败。

> const {prop} = undefined
TypeError: Cannot destructure property 'prop' of 'undefined'
as it is undefined.

> const {prop} = null
TypeError: Cannot destructure property 'prop' of 'null'
as it is null.

37.8.2 不能对不可迭代的值进行数组解构

数组解构要求解构的值是可迭代的。 因此,您不能对 `undefined` 和 `null` 进行数组解构。 但是您也不能对不可迭代的对象进行数组解构

> const [x] = {}
TypeError: {} is not iterable

  测验:基础

请参阅测验应用程序

37.9 (进阶)

以下所有部分均为进阶内容。

37.10 默认值

通常,如果模式没有匹配项,则相应的变量将设置为 `undefined`

const {prop: p} = {};
assert.equal(p, undefined);

如果您想使用不同的值,则需要指定*默认值*(通过 `=`)

const {prop: p = 123} = {}; // (A)
assert.equal(p, 123);

在 A 行中,我们将 `p` 的默认值指定为 `123`。 使用该默认值是因为我们正在解构的数据没有名为 `prop` 的属性。

37.10.1 数组解构中的默认值

在这里,我们有两个默认值分配给变量 `x` 和 `y`,因为相应的元素在被解构的数组中不存在。

const [x=1, y=2] = [];

assert.equal(x, 1);
assert.equal(y, 2);

数组模式的第一个元素的默认值为 `1`;第二个元素的默认值为 `2`。

37.10.2 对象解构中的默认值

您还可以为对象解构指定默认值

const {first: f='', last: l=''} = {};
assert.equal(f, '');
assert.equal(l, '');

被解构的对象中既不存在属性键 `first` 也不存在属性键 `last`。 因此,使用默认值。

使用属性值简写,此代码变得更简单

const {first='', last=''} = {};
assert.equal(first, '');
assert.equal(last, '');

37.11 参数定义类似于解构

考虑到我们在本章中学到的内容,参数定义与数组模式(剩余元素、默认值等)有很多共同点。 实际上,以下两个函数声明是等效的

function f1(«pattern1», «pattern2») {
  // ···
}

function f2(...args) {
  const [«pattern1», «pattern2»] = args;
  // ···
}

37.12 嵌套解构

到目前为止,我们只在解构模式中使用变量作为*赋值目标*(数据接收器)。 但是您也可以使用模式作为赋值目标,这使您能够将模式嵌套到任意深度

const arr = [
  { first: 'Jane', last: 'Bond' },
  { first: 'Lars', last: 'Croft' },
];
const [, {first}] = arr; // (A)
assert.equal(first, 'Lars');

在 A 行的数组模式中,索引 1 处有一个嵌套的对象模式。

嵌套模式可能会变得难以理解,因此最好适度使用。

  测验:进阶

请参阅测验应用程序