原文链接:http://javascript.info/destructuring-assignment,translate with ❤️ by zhangbao.
在 JavaScript 中,最经常使用的两个数据结构是 Object 和 Array。
对象允许我们把许多片段信息整合到一个实体里面去,数组允许我们存储有序集合。所以我们可以制作一个对象或一个数组,把它作为一个单独的实体来处理,或者把它传递给一个函数调用。
解构赋值是一个特殊的、用来“打开”数组和对象的方法,并整合进一堆变量里,这有时很便捷。结构赋值也适用于具有很多参数、具有默认值的复杂函数里,很快我们就会看到它们是如何处理的。
数组解构
下面例子,用来说明如何将数组拆开放在一些变量里:
// we have an array with the name and surname
let arr = ["Ilya", "Kantor"]
// destructuring assignment
let [firstName, surname] = arr;
alert(firstName); // Ilya
alert(surname); // Kantor
现在我们可以使用变量而不是通过数组成员。
当与 split 或其他返回数组的方法相结合时,这种语法形式看起来很棒:
let [firstName, surname] = "Ilya Kantor".split(' ');
⚠️“解构”不是“破坏”
它被称为“解构赋值”,是因为它通过将条目“解开”复制到变量中,但数组本身不会修改。
这只是下面方式的一种简写形式:
// let [firstName, surname] = arr;
let firstName = arr[0];
let surname = arr[1];
⚠️忽略第一个元素
数组中不需要的元素也可以通过一个额外的逗号来丢弃:
// first and second elements are not needed
let [, , title] = ["Julius", "Caesar", "Consul", "of the Roman Republic"];
alert( title ); // Consul
在上面的代码里,第一、第二个数组元素被忽略了,第三个元素赋值为 title,剩下的元素都会忽略了。
⚠️只要表达式右边是可迭代对象,都可以用解构赋值
解构赋值不仅适用于数组,只要表达式右边是一个可迭代对象,都可以用解构赋值。
let [a, b, c] = "abc"; // ["a", "b", "c"]
let [one, two, three] = new Set([1, 2, 3]);
⚠️左边的赋值也可以是任意类型的
在等式左边,我们可以使用任何“可赋值”的元素。
例如,一个对象属性:
let user = {};
[user.name, user.surname] = "Ilya Kantor".split(' ');
alert(user.name); // Ilya
⚠️与 .entries() 配合使用
在之前的章节里,我们有使用 Object.entries(obj) 方法。
我们可以用它来解构一个对象的键和值:
let user = {
name: "John",
age: 30
};
// loop over keys-and-values
for (let [key, value] of Object.entries(user)) {
alert(`${key}:${value}`); // name:John, 然后是 age:30
}
map 也一样起作用:
let user = new Map();
user.set("name", "John");
user.set("age", "30");
for (let [key, value] of user.entries()) {
alert(`${key}:${value}`); // name:John, then age:30
}
剩余符号 “…”
我们不仅可以获得第一个值,也可以收集所有剩余的值——我们可以使用三个点“…”来收集“剩余”的参数。
let [name1, name2, ...rest] = ["Julius", "Caesar", "Consul", "of the Roman Republic"];
alert(name1); // Julius
alert(name2); // Caesar
alert(rest[0]); // Consul
alert(rest[1]); // of the Roman Republic
alert(rest.length); // 2
rest 的值是持有剩余数组元素的数组。我们可以用任何其他的变量名来替代 rest 这个变量名,主要保证变量名之前使用三个点,而且是作为解构赋值的最后一个参数就行了。
默认值
如果数组中的值比赋值中的变量少,那么也不会发生错误。缺少的值会被赋值为 undefiend:
let [firstName, surname] = [];
alert(firstName); // undefined
如果我们要用“默认”值来替代缺失值,可以用 = 提供默认值:
// 默认值
let [name = "Guest", surname = "Anonymous"] = ["Julius"];
alert(name); // Julius (从数组里获取的)
alert(surname); // Anonymous (使用了默认值)
默认值可以是更复杂的表达式,甚至是函数调用。只有在没有提供值的情况下才会使用它们。
例如,在这里我们使用 prompt 函数来提供两个默认值,但它只会在数值缺失时使用:
// 默认值
let [name = "Guest", surname = "Anonymous"] = ["Julius"];
alert(name); // Julius (从数组里获取的)
alert(surname); // Anonymous (使用了默认值)
对象解构
解构赋值对对象同样生效。
基本语法是:
let {var1, var2} = {var1:…, var2…}
右边是已经存在的一个对象,我们将其解构到几个变量里。左边包含对应属性的“模式”。在这个简单的例子里,它表示所有在 {…} 中的属性名。
例如:
let options = {
title: "Menu",
width: 100,
height: 200
};
let {title, width, height} = options;
alert(title); // Menu
alert(width); // 100
alert(height); // 200
属性 options.title,options.width 和 options.heght 被赋值到了对应的变量中。顺序不重要:
// 改变 let {...} 变量里属性的声明顺序
let {height, width, title} = { title: "Menu", height: 200, width: 100 }
左边的模式可能更复杂,用来指定属性和变量之间的映射。
如果我们想用另一个名字来接收某个属性的值,例如,options.width 属性值放在了变量 w 中,可以使用冒号来标记默认值:
let options = {
title: "Menu",
width: 100,
height: 200
};
// { sourceProperty: targetVariable }
let {width: w, height: h, title} = options;
// width -> w
// height -> h
// title -> title
alert(title); // Menu
alert(w); // 100
alert(h); // 200
冒号表示“谁:去哪儿”。上面例子里,属性 width 去到了 w 中,属性 height 去到了 h 中,title 属性去到了同名的变量中。
对于潜在的缺失属性,我们可以使用“=”来设置默认值,像这样:
let options = {
title: "Menu"
};
let {width = 100, height = 200, title} = options;
alert(title); // Menu
alert(width); // 100
alert(height); // 200
类似于数组或者函数参数,默认值可以是任何表达式甚至是函数调用。它们在值没有提供时,就会被使用。
下面的代码会询问 width,但不会询问 title。
let options = {
title: "Menu"
};
let {width = prompt("width?"), title = prompt("title?")} = options;
alert(title); // Menu
alert(width); // (弹窗询问时,你输入的值)
我们也可以把冒号和等号接合起来使用:
let options = {
title: "Menu"
};
let {width: w = 100, height: h = 200, title} = options;
alert(title); // Menu
alert(w); // 100
alert(h); // 200
rest 操作符
如果对象的属性数量大于我们拥有的变量数量?我们怎么可以把对象“剩下的”属性保存在一个地方呢?
在这里使用 rest 运算符(三个点)的规范几乎是标准的,但是大多数浏览器还不支持它。
它像这样使用:
let options = {
title: "Menu",
height: 200,
width: 100
};
let {title, ...rest} = options;
// 现在 title="Menu", rest={height: 200, width: 100}
alert(rest.height); // 200
alert(rest.width); // 100
⚠️没有 let 的问题
在上面的例子中,所有变量都是在赋值之前被声明的:let {…} = {…}。但是我们也可以使用已经存在的变量,但是有一个问题。
是无法正常工作的:
let title, width, height;
// 这一行会发生错误
{title, width, height} = {title: "Menu", width: 200, height: 100};
问题是 JavaScript 将 {…} 看成是主代码流中(不是在另一个表达式中)中的一个代码块。这样的代码块可以用来对语句进行分组,如下所述:
{
// 一个代码块
let message = "Hello";
// ...
alert( message );
}
为了显示 它不是一个代码块,我们可以将整个任务包装在一个圆括号中 (…):
let title, width, height;
// 现在 OK 了
({title, width, height} = {title: "Menu", width: 200, height: 100});
alert( title ); // Menu
内嵌解构
如果一个对象或者数组包含其他对象和数组,我们可以使用更加复杂的左侧模式解析更加深层的部分。
在下面的代码里, options 有一个属性 size 保存的值是一个对象;还有一个属性 items,保存的值是一个数组。赋值左侧的模式具有相同的结构:
let options = {
size: {
width: 100,
height: 200
},
items: ["Cake", "Donut"],
extra: true // something extra that we will not destruct
};
// destructuring assignment on multiple lines for clarity
let {
size: { // 在这里给 size 赋值
width,
height
},
items: [item1, item2], // 在这里给 items 赋值
title: t = "Menu" // 没有在 object 里出现(就会使用默认值)
} = options;
alert(title); // Menu
alert(width); // 100
alert(height); // 200
alert(item1); // Cake
alert(item2); // Donut
整个 options 对象中除了 extra 没有提到,都会复制到对应的变量里。
最终,我们会得到变量 width,height,item1,item2 和使用了默认值的 title。
这经常发生在解构赋值中。我们有一个复杂的、拥有许多属性的对象,然后想从中解析出一些变量。
甚至这里发生:
// 将 size 作为一个整体赋值给一个变量, 忽略剩余的
let { size } = options;
智能函数参数
有时一个函数具有多个参数,其中多数参数都是可选择的。这对于用户界面来说尤其如此。想象一个创建菜单的函数,它可能接收一个宽度,一个高度,一个标题,项目列表等等。
这是一个写这样的函数的坏方法:
function showMenu(title = "Untitled", width = 200, height = 100, items = []) {
// ...
}
在现实世界里,问题是我们怎么记住这些参数顺序呢。这一点 IDE 通常会给我们帮助,特别是在代码在非常好的组织结构下,但是仍然……另一个问题是,当大多数参数在默认情况下都是 ok 时,如何调用函数。
像这样?
showMenu("My Menu", undefined, undefined, ["Item1", "Item2"])
这是丑陋的。当我们处理更多的参数时,它变得不易读。
解构来拯救!
们可以将参数作为一个对象传递,函数会立即将它们解构为变量:
// 我们向函数传递一个对象
let options = {
title: "My menu",
items: ["Item1", "Item2"]
};
// ...立即将它展开成变量
function showMenu({title = "Untitled", width = 200, height = 100, items = []}) {
// title, items – 从 options 中获取,
// width, height – 使用默认值
alert( `${title} ${width} ${height}` ); // My Menu 200 100
alert( items ); // Item1, Item2
}
showMenu(options);
我们还可以使用嵌套对象和冒号映射的更复杂的解构形式:
let options = {
title: "My menu",
items: ["Item1", "Item2"]
};
function showMenu({
title = "Untitled",
width: w = 100, // width 跑到了 w 里
height: h = 200, // height 跑到了 h 里
items: [item1, item2] // items 第一个元素进入 item1, 第二个元素进入 item2
}) {
alert( `${title} ${w} ${h}` ); // My Menu 100 200
alert( item1 ); // Item1
alert( item2 ); // Item2
}
showMenu(options);
其语法与结构赋值的语法相同:
function({
incomingProperty: parameterName = defaultValue
...
})
请注意,这样的破坏假设 showMenu() 确实有一个参数。如果我们在默认情况下想使用所有默认值,那么我们应该指定一个空对象:
showMenu({});
showMenu(); // this would give an error
我们可以为整个赋值对象使用一个默认值,来解决这个问题:
// 简化参数,使其更清晰
function showMenu({ title = "Menu", width = 100, height = 200 } = {}) {
alert( `${title} ${width} ${height}` );
}
showMenu(); // Menu 100 200
在上面的代码里,整个参数对象有一个可用的 {} 作为初始值,所以总有值用来解构的。
总结
解构赋值立即将对象或数组映射到许多变量上。
对象语法:
let {prop : varName = default, ...} = object
这表示变量 prop 会进入到变量 varName 中。如果这个参数不存在的话,就使用 default 作为默认参数值。
- 数组语法:
let [item1 = default, item2, ...rest] = array
第一个数组成员进入到 item1,第二个数组成员进入到 item2,所有剩下的数组成员进入到数组 rest 里。
- 针对更加复杂的情形,左边的结构应该与右边的结构保持一致。
(完)