深入 JavaScript
请支持本书:购买捐赠
(广告,请勿屏蔽。)

3 解构算法



在本章中,我们将从不同的角度来看待解构:将其视为递归模式匹配算法。

该算法将使我们更好地理解默认值。这在最后将很有用,我们将尝试弄清楚以下两个函数有何不同

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

3.1 为模式匹配算法做准备

解构赋值如下所示

«pattern» = «value»

我们想使用 patternvalue 中提取数据。

现在,我们将研究一种用于执行此类赋值的算法。该算法在函数式编程中称为*模式匹配*(简称:*匹配*)。它指定了运算符 (“匹配”),该运算符将 patternvalue 进行匹配,并在执行此操作时分配给变量

«pattern» ← «value»

我们将仅探讨解构赋值,但解构变量声明和解构参数定义的工作原理类似。我们也不会深入探讨高级功能:计算属性键、属性值简写以及作为赋值目标的对象属性和数组元素超出了本章的范围。

匹配运算符的规范由声明式规则组成,这些规则会深入到两个操作数的结构中。声明式符号可能需要一些时间来适应,但它使规范更加简洁。

3.1.1 使用声明式规则指定匹配算法

本章中使用的声明式规则对输入进行操作,并通过副作用产生算法的结果。这是一个这样的规则(我们稍后会再次看到)

此规则具有以下部分

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

让我们考虑本章中的另一条规则

在规则(2e)中,头部表示如果将空对象模式 {} 与值 obj 进行匹配,则执行此规则。主体表示,在这种情况下,我们就完成了。

规则(2c)和规则(2e)共同构成一个声明式循环,该循环迭代箭头左侧模式的属性。

3.1.2 根据声明式规则评估表达式

完整的算法是通过一系列声明式规则指定的。假设我们要评估以下匹配表达式

{first: f, last: l} ← obj

要应用一系列规则,我们从上到下遍历它们,并执行第一个适用的规则。如果该规则的主体中存在匹配表达式,则将再次应用这些规则。依此类推。

有时,头部包含一个条件,该条件也决定了规则是否适用 - 例如

3.2 模式匹配算法

3.2.1 模式

模式是以下之一

接下来的三节将指定在匹配表达式中处理这三种情况的规则。

3.2.2 变量规则

3.2.3 对象模式规则

规则 2a 和 2b 处理非法值。规则 2c-2e 循环遍历模式的属性。在规则 2d 中,我们可以看到,如果 obj 中没有匹配的属性,则默认值提供了一个可供匹配的替代方案。

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

迭代器完成类似于对象中缺少属性。

3.3 空对象模式和数组模式

算法规则的有趣结果:我们可以使用空对象模式和空数组模式进行解构。

给定一个空对象模式 {}:如果要解构的值既不是 undefined 也不是 null,则什么也不会发生。否则,将抛出 TypeError

const {} = 123; // OK, neither undefined nor null
assert.throws(
  () => {
    const {} = null;
  },
  /^TypeError: Cannot destructure 'null' as it is null.$/)

给定一个空数组模式 []:如果要解构的值是可迭代的,则什么也不会发生。否则,将抛出 TypeError

const [] = 'abc'; // OK, iterable
assert.throws(
  () => {
    const [] = 123; // not iterable
  },
  /^TypeError: 123 is not iterable$/)

换句话说:空解构模式强制值具有某些特征,但没有其他影响。

3.4 应用算法

在 JavaScript 中,命名参数是通过对象模拟的:调用者使用对象字面量,而被调用者使用解构。此模拟在“面向急躁程序员的 JavaScript” 中有详细说明。以下代码显示了一个示例:函数 move1() 有两个命名参数,xy

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

在 A 行中有三个默认值

但是,为什么我们要像上一个代码片段那样定义参数呢?为什么不像下面这样呢?

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

为了了解为什么 move1() 是正确的,我们将在两个示例中使用这两个函数。在此之前,让我们看看如何通过匹配来解释参数的传递。

3.4.1 背景:通过匹配传递参数

对于函数调用,*形式参数*(在函数定义内部)与*实际参数*(在函数调用内部)进行匹配。例如,以下面的函数定义和函数调用为例。

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

参数 ab 的设置方式类似于以下解构。

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

3.4.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,这不是我们想要的。问题是 {x,y} 不再与默认值匹配,而是与 {z:3} 匹配。

3.4.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

它按预期工作!这一次,将 xy{z:3} 匹配的模式不是问题,因为它们有自己的局部默认值。

3.4.4 结论:默认值是模式部分的特性

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