32.1 第一次尝试解构
通过普通赋值方式,你一次只能获取数据中的一个元素,例如,通过:
const arr = ['a', 'b', 'c'];
const x = arr[0]; // 只能获取第一个
const y = arr[1]; // 只能获取第二个
通过解构方式,你可以同时获取数据中的几个元素,通过在接收数据的位置进行模式匹配。在上方的代码的=
左侧就是这样一个位置。在下方的代码中,行A中的方括号就是一个解构模式:
const arr = ['a', 'b', 'c'];
const [x, y] = arr; // (A)
assert.equal(x, 'a');
assert.equal(y, 'b');
这段代码做了和之前的代码同样的事。
注意,这种方式比整个数据要“小巧”:我们只获取我们需要的数据元素。(译者注:冒号后面的话才是核心)
32.2 构造与提取
为了明白什么是解构,假想JavaScript有两种截然相反的操作:
- 你可以 构造 复合数据,例如通过设置属性和对象文字的方式。
- 你可以从复合数据中 提取 数据,例如通过获取属性的方式。
构造数据看起来像这样:
// 一次添加一个属性
const jane1 = {};
jane1.first = 'Jane';
jane1.last = 'Doe';
// 一次添加很多属性
const jane2 = {
first: 'Jane',
last: 'Doe',
};
assert.deepEqual(jane1, jane2);
提取数据看起来像这样:
const jane = {
first: 'Jane',
last: 'Doe',
};
// 一次获取一个属性
const f1 = jane.first;
const l1 = jane.last;
assert.equal(f1, 'Jane');
assert.equal(l1, 'Doe');
// 一次获取多个属性(新方式!)
const {first: f2, last: l2} = jane; // (A)
assert.equal(f2, 'Jane');
assert.equal(l2, 'Doe');
行A中的操作方式是全新的:我们声明两个变量f2
和l2
,并通过 解构 方式(多值提取)初始化它们。下方行A的部分是一个 解构 模式:
{first: f2, last: l2}
解构模式在语法上类似于用于多值构造的写法。但是它们出现在接收数据的地方(在赋值语句的左边),而不是创建数据的地方(在赋值语句的右边)。
32.3 我们在哪里可以使用解构?
解构模式可用于“数据接收位置”,例如:
- 变量声明:
const [a] = ['x']; assert.equal(a, 'x'); let [b] = ['y']; assert.equal(b, 'y');
- 赋值语句:
let b; [b] = ['z']; assert.equal(b, 'z');
- 参数定义:
const f = ([x]) => x; assert.equal(f(['a']), 'a');
注意,变量的定义包括在for-of
循环中的const
和let
声明:
const arr = ['a', 'b'];
for (const [index, element] of arr.entries()) {
console.log(index, element);
}
// Output:
// 0, 'a'
// 1, 'b'
在接下来的两部分中,我们将深入研究两种类型的解构:对象解构和数组解构。
32.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');
为什么这样可以?因为数组索引也是属性。
32.4.1 属性值缩写
对象写法支持属性值缩写,对象解构模式也支持:
const { street, city } = address;
assert.equal(street, 'Evergreen Terrace');
assert.equal(city, 'Springfield');
32.4.2 Rest属性
在对象写法中,你可以拥有扩展运算符。在对象模式中,可以使用rest属性(必须放在最后):
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});
一个rest参数,例如remaining
(A行)被赋予一个对象,该对象具有模式中没有提到键的所有数据属性。
remaining
也可以看作是从obj
中非破坏性地删除属性a
的结果。(译者注:不再需要a
属性,但不想改变原对象obj
,得到一个新的不包含a
的数组remaining
)
32.4.3 语法陷阱:通过对象解构来赋值
如果我们在赋值过程中使用对象解构,我们就会面临语法歧义带来的陷阱——你不能用大括号来开始一个语句,因为JavaScript会认为你在开始一个代码块:
let prop;
assert.throws(
() => eval("{prop} = { prop: 'hello' };"),
{
name: 'SyntaxError',
message: 'Unexpected token =',
});
为什么
eval()
?
eval()
会延迟解析(因此也会延迟SyntaxError
),直到执行assert.throw()
的回调。如果我们不使用它,在解析这段代码时就会得到一个错误,assert.throw()
甚至不会执行。
解决办法是把整个赋值语句放在括号里:
let prop;
({prop} = { prop: 'hello' });
assert.equal(prop, 'hello');
32.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的数组元素被忽略的原因。
32.5.1 数组解构可以用于任何可遍历的数据结构
数组解构可以用于任何可遍历的值,而不仅仅是数组:
// Sets数据结构是iterable的
const mySet = new Set().add('a').add('b').add('c');
const [first, second] = mySet;
assert.equal(first, 'a');
assert.equal(second, 'b');
// 字符串数据结构是iterable的
const [a, b] = 'xyz';
assert.equal(a, 'x');
assert.equal(b, 'y');
32.5.2 Rest参数
在数组文字中,你可以使用扩展运算符。在数组模式中,你可以使用rest参数(必须放在最后):
const [x, y, ...remaining] = ['a', 'b', 'c', 'd']; // (A)
assert.equal(x, 'a');
assert.equal(y, 'b');
assert.deepEqual(remaining, ['c', 'd']);
rest元素变量,例如remaining
(A行)被赋予一个数组,其中包含尚未提到的已解构值的所有元素。
32.6 解构的一些例子
32.6.1 数组解构:交换变量的值
你可以使用数组解构来交换两个变量的值,而不需要临时变量:
let x = 'a';
let y = 'b';
[x,y] = [y,x]; // swap
assert.equal(x, 'b');
assert.equal(y, 'a');
32.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');
32.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)) {
// 有所发现的话
return { value, index };
}
}
// 什么都没找到的话
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);
当我们使用属性的键名时,我们声明值和索引时的顺序并不重要:
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);
这些方便结合在一起,使得这种处理多个返回值的方法非常通用。
32.7 如果一个模式的一部分不匹配,会发生什么?
如果一个模式的一部分不匹配,会发生什么?如果使用非批处理操作符也会发生同样的事情:您将得到undefined
的结果。
32.7.1 对象解构时缺少属性
如果对象模式中的属性在右侧没有匹配项,则得到undefined
:
const {prop: p} = {};
assert.equal(p, undefined);
32.7.2 数组解构时缺少元素
如果数组模式中的元素在右侧没有匹配项,则得到undefined
:
const [x] = [];
assert.equal(x, undefined);
32.8 什么值无法被解构?
32.8.1 你不能对undefined
和null
使用对象解构
只有当要解构的值未定义(undefined
)或为空(null
)时,对象解构才会失败。也就是说,当通过点操作符访问属性时也会失败。
assert.throws(
() => { const {prop} = undefined; },
{
name: 'TypeError',
message: "Cannot destructure property `prop` of " +
"'undefined' or 'null'.",
}
);
assert.throws(
() => { const {prop} = null; },
{
name: 'TypeError',
message: "Cannot destructure property `prop` of " +
"'undefined' or 'null'.",
}
);
32.8.2 你不能对无法遍历的值进行数组解构
数组解构要求解构值是可遍历的。因此,不能对未定义(undefined
)或者空(null
)的数组进行解构,同样你也不能解构无法遍历的对象:
assert.throws(
() => { const [x] = {}; },
{
name: 'TypeError',
message: '{} is not iterable',
}
);
测试:基本
请看 quiz app
32.9 (高级用法)
余下部分皆为高级用法。
32.10 默认值
一般情况下,如果一个模式没有匹配,则将对应的变量设置为undefined
:
const {prop: p} = {};
assert.equal(p, undefined);
如果你想要设定一个同样用途的不同的值,你需要指定一个初始值(通过=
):
const {prop: p = 123} = {}; // (A)
assert.equal(p, 123);
在行A中,我们指定p
的默认值为123
。该默认值会生效,因为要解构的的数据中没有名为prop
的属性。
32.10.1 默认值在数组解构中的用法
在这里,我们有两个默认值被赋给变量x
和y
,因为被解构的数组中不存在相应的元素。
const [x=1, y=2] = [];
assert.equal(x, 1);
assert.equal(y, 2);
给数组模式的第一个元素的默认值是1
,给第二个元素的值是2
。
32.10.2 默认值对象解构中的用法
你也可以为对象解构指定默认值:
const {first: f='', last: l=''} = {};
assert.equal(f, '');
assert.equal(l, '');
属性键first
和属性键last
都不存在于被解构的对象中。因此,默认值会生效。使用属性值缩写,代码会变得更简单:
const {first='', last=''} = {};
assert.equal(first, '');
assert.equal(last, '');
32.11 参数定义和解构的相似性
考虑到我们在本章所学到的内容,参数定义与数组模式(rest元素、默认值等)有很多共同点。事实上,以下两个函数声明是等价的:
function f1(«pattern1», «pattern2») {
// ···
}
function f2(...args) {
const [«pattern1», «pattern2»] = args;
// ···
}
32.12 嵌套解构
到目前为止,我们只在解构模式中使用变量作为 赋值目标 (数据接收方)。但是你也可以使用模式作为赋值目标,这使你能够将模式嵌套到任意深度:
const arr = [
{ first: 'Jane', last: 'Bond' },
{ first: 'Lars', last: 'Croft' },
];
const [, {first}] = arr; //(A)
assert.equal(first, 'Lars');
在A行的数组模式中,有一个数组嵌套,位于索引1。(译者注:即{first}
,实际上作者并没有注释A行在何处,是我后来添加上去的。或许作者是故意的呢?哈哈。)
嵌套模式不太容易理解,所以最好适当地使用。
测试:高级用法
请看quiz app