10.11 解构算法
本节从不同的角度来看解构过程:一个递归的模式匹配算法。
这个不同的角度应该对理解默认值很有帮助。如果你感觉自己没有完全理解解构过程,那么继续读下去。
在最后,我将会使用这个算法解释下面两个函数声明的不同之处。
function move({x=0, y=0} = {}) { ··· }
function move({x, y} = { x: 0, y: 0 }) { ··· }
10.11.1 算法
解构赋值看起来像这样:
«pattern» = «value»
我们想使用 pattern
从 value
里面获取数据。我现在将会描述一种算法来做这件事,这在函数式编程中被称为模式匹配( pattern matching )(简称:匹配)。该算法指定操作符←
(“匹配”)用于表示解构赋值中给 pattern
匹配上一个 value
并赋值给变量:
«pattern» ← «value»
该算法通过递归的规则执行,这些递归规则在←操作符两侧的操作数都会发生。这个声明性的符号可能不好适应,但是它让该算法的说明更加简洁明了。每一个规则都有两部分:
- 头部指明该规则应用到的操作数。
- 主体部分指明接下来做什么。
我仅展示解构赋值的算法。解构变量声明和解构参数定义是类似的。
我也不会讲解高级的特性(计算属性键;属性值缩写;对象属性和数组元素作为赋值目标),仅仅讲解基础的东西。
10.11.1.1 模式
一个模式是下列的几种形式之一:
- 一个变量:
x
- 一个对象模式:
{«properties»}
- 一个数组模式:
[«elements»]
后面每节描述了这三种情况中的一种。
10.11.1.2 变量
- (1)
x ← value
(包括undefined
和null
)
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
let tmp = obj.key;
if (tmp !== undefined) {
«pattern» ← tmp
} else {
«pattern» ← default_value
}
{«properties»} ← obj
- (2e)
{} ← obj
// No properties left, nothing to do
10.11.1.4 数组模式
数组模式和迭代器。数组解构算法以数组模式和一个迭代器开始:
- (3a)
[«elements»] ← non_iterable
assert(!isIterable(non_iterable))
throw new TypeError();
- (3b)
[«elements»] ← iterable
assert(isIterable(iterable))
let 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 after last item
«elements» ← iterator
- (3d)
«pattern» = default_value, «elements» ← iterator
let tmp = getNext(iterator); // undefined after last item
if (tmp !== undefined) {
«pattern» ← tmp
} else {
«pattern» ← default_value
}
«elements» ← iterator
- (3e)
, «elements» ← iterator (hole, elision)
getNext(iterator); // skip
«elements» ← iterator
- (3f)
...«pattern» ← iterator
(总是最后一部分!)
let tmp = [];
for (let elem of iterator) {
tmp.push(elem);
}
«pattern» ← tmp
- (3g)
← iterator
辅助函数:
function getNext(iterator) {
let {done,value} = iterator.next();
return (done ? undefined : value);
}
10.11.2 应用算法
下面的函数定义有命名的参数,一种有时称作可选对象( options object )的技术,在参数处理章节
有解释。参数使用了解构和默认值,这样 x
和 y
就可以省略不传了。但是对象参数也可以不传,就像下面代码中最后一行一样。此特性通过在函数声明头部的={}
起作用。
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);
参数 a
和 b
和下面的解构过程类似地被设置。
[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 }
该解构过程导致下面的两个赋值(规则 2c ,1):
x = 0;
y = 0;
这就是仅有的使用默认值的情况。
示例2。让我们看看函数调用 move2({z:3})
,这会导致如下的解构过程:
[{x, y} = { x: 0, y: 0 }] ← [{z:3}]
在右侧数组的索引0处有一个数组元素。因此,默认值会被忽略,下一步是(规则 3d ):
{x, y} ← { z: 3 }
这会导致 x
和 y
都被设置为 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}]
就像示例1一样,属性 x
和 y
在右侧没有匹配的内容,所以使用默认值:
x = 0
y = 0
10.11.3 结论
这些例子展示了默认值是模式部分(对象属性或者数组元素)的特性。如果某个部分没有匹配上,或者匹配到了 undefined
,那么默认值就生效了。也就是说,模式匹配上了默认值。