请多多支持本书:购买 (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] =
/^(dddd)-(dd)-(dd)$/
.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) {}
当遇到 undefined
或 null
时, ToObject()
会抛一个 TypeError
错误。因此,下面的解构在访问任何属性之前就失败了:
const { prop: x } = undefined; // TypeError const { prop: y } = null; // TypeError
所以,你可以使用空对象模式 {}
检查一个值能否强制转换成对象。我们已经知道,只有 undefined
和 null
不能:
({} = [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] = 'auD83DuDCA9c'; // x='a'; y='uD83DuDCA9'; 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
但是,要注意顺序:变量 x
和 y
是从左到右声明的,假如在变量声明之前访问它,就会产生 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
如果前面带上 let
, var
和 const
的话,则可以放心使用花括号:
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] =
/^(dddd)-(dd)-(dd)$/
.exec('2999-12-31');
假如你只想得到正则里的分组(而不是匹配的整体, all
),你可以使用省略,略过下标为 0 的数组元素:
const [, year, month, day] =
/^(dddd)-(dd)-(dd)$/
.exec('2999-12-31');
假如正则表达式不能成功匹配,exec()
会返回 null
。遗憾的是,由于返回 null
无法给变量设置默认值,所以此时需要用或操作符(||
):
const [, year, month, day] =
/^(dddd)-(dd)-(dd)$/
.exec(someStr) || [];
Array.prototype.split()
会返回一个数组。因此,假如你只关心元素而不关心数组的话,可以用解构:
const cells = 'Jane Doe CTO' const [firstName, lastName, title] = cells.split(' '); console.log(firstName, lastName, title);
10.10.2 解构返回的对象
解构可用于从函数或者方法返回的对象中提取数据。比如,迭代器方法 next()
返回一个对象,该对象有两个属性, done
和 value
。下面的代码通过迭代器 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
由于 index
和 element
都指向属性名,所以可以不分先后顺序:
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»
我们要使用 pattern
从 value
中提取数据。我先描述一下实现这个功能的算法,在函数式编程中该算法叫做 模式匹配(简称:_匹配_)。该算法将一个操作符 ←
(“匹配”)指定给解构赋值,这个解构赋值会用一个 pattern
去匹配一个 value
,同时赋值给变量:
«pattern» ← «value»
该算法是通过一些迭代的规则来定义的,这些规则会分别解析 ←
操作符两边的运算元。你可能还不习惯这个声明符号,但是这个符号能让算法的定义更简洁。每个迭代规则由以下两部分构成:
-
头部(head)指明了规则所操作的运算元。
-
主体部分(body)指定了下一步要执行的动作。
这里只展示解构赋值的算法。解构变量声明和解构参数定义的算法跟这个算法很相似。
我也不会介绍更高级的特性(计算属性键;属性值缩写;赋值目标的对象属性和数组元素)。这里只介绍基础知识。
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
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),在参数处理这一章有详细解释。参数使用了解构和默认值,这样一来,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()
,解构是如何进行的。
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 }
这会把 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}]
数组模式的第一个元素在右侧有匹配值,使用该匹配值进行解构(规则 3d):
{x=0, y=0} ← {z:3}
跟例 1 相似,右侧不存在属性 x
和 y
,因此使用默认值:
x = 0
y = 0
10.11.3 结论
以上例子展示了默认值属于模式部分(对象属性或者数组元素)的一个特性。假如模式的某一部分没有匹配值或者匹配了 undefined
,那么就会使用默认值。换句话说,模式匹配了默认值。
下一章: 11. 参数处理