原文链接:http://javascript.info/destructuring-assignment,translate with ❤️ by zhangbao.

在 JavaScript 中,最经常使用的两个数据结构是 Object 和 Array。

对象允许我们把许多片段信息整合到一个实体里面去,数组允许我们存储有序集合。所以我们可以制作一个对象或一个数组,把它作为一个单独的实体来处理,或者把它传递给一个函数调用。

解构赋值是一个特殊的、用来“打开”数组和对象的方法,并整合进一堆变量里,这有时很便捷。结构赋值也适用于具有很多参数、具有默认值的复杂函数里,很快我们就会看到它们是如何处理的。

数组解构

下面例子,用来说明如何将数组拆开放在一些变量里:

  1. // we have an array with the name and surname
  2. let arr = ["Ilya", "Kantor"]
  3. // destructuring assignment
  4. let [firstName, surname] = arr;
  5. alert(firstName); // Ilya
  6. alert(surname); // Kantor

现在我们可以使用变量而不是通过数组成员。

当与 split 或其他返回数组的方法相结合时,这种语法形式看起来很棒:

  1. let [firstName, surname] = "Ilya Kantor".split(' ');

⚠️“解构”不是“破坏”

它被称为“解构赋值”,是因为它通过将条目“解开”复制到变量中,但数组本身不会修改。

这只是下面方式的一种简写形式:

  1. // let [firstName, surname] = arr;
  2. let firstName = arr[0];
  3. let surname = arr[1];

⚠️忽略第一个元素

数组中不需要的元素也可以通过一个额外的逗号来丢弃:

  1. // first and second elements are not needed
  2. let [, , title] = ["Julius", "Caesar", "Consul", "of the Roman Republic"];
  3. alert( title ); // Consul

在上面的代码里,第一、第二个数组元素被忽略了,第三个元素赋值为 title,剩下的元素都会忽略了。

⚠️只要表达式右边是可迭代对象,都可以用解构赋值

解构赋值不仅适用于数组,只要表达式右边是一个可迭代对象,都可以用解构赋值。

  1. let [a, b, c] = "abc"; // ["a", "b", "c"]
  2. let [one, two, three] = new Set([1, 2, 3]);

⚠️左边的赋值也可以是任意类型的

在等式左边,我们可以使用任何“可赋值”的元素。

例如,一个对象属性:

  1. let user = {};
  2. [user.name, user.surname] = "Ilya Kantor".split(' ');
  3. alert(user.name); // Ilya

⚠️与 .entries() 配合使用

在之前的章节里,我们有使用 Object.entries(obj) 方法。

我们可以用它来解构一个对象的键和值:

  1. let user = {
  2. name: "John",
  3. age: 30
  4. };
  5. // loop over keys-and-values
  6. for (let [key, value] of Object.entries(user)) {
  7. alert(`${key}:${value}`); // name:John, 然后是 age:30
  8. }

map 也一样起作用:

  1. let user = new Map();
  2. user.set("name", "John");
  3. user.set("age", "30");
  4. for (let [key, value] of user.entries()) {
  5. alert(`${key}:${value}`); // name:John, then age:30
  6. }

剩余符号 “…”

我们不仅可以获得第一个值,也可以收集所有剩余的值——我们可以使用三个点“…”来收集“剩余”的参数。

  1. let [name1, name2, ...rest] = ["Julius", "Caesar", "Consul", "of the Roman Republic"];
  2. alert(name1); // Julius
  3. alert(name2); // Caesar
  4. alert(rest[0]); // Consul
  5. alert(rest[1]); // of the Roman Republic
  6. alert(rest.length); // 2

rest 的值是持有剩余数组元素的数组。我们可以用任何其他的变量名来替代 rest 这个变量名,主要保证变量名之前使用三个点,而且是作为解构赋值的最后一个参数就行了。

默认值

如果数组中的值比赋值中的变量少,那么也不会发生错误。缺少的值会被赋值为 undefiend:

  1. let [firstName, surname] = [];
  2. alert(firstName); // undefined

如果我们要用“默认”值来替代缺失值,可以用 = 提供默认值:

  1. // 默认值
  2. let [name = "Guest", surname = "Anonymous"] = ["Julius"];
  3. alert(name); // Julius (从数组里获取的)
  4. alert(surname); // Anonymous (使用了默认值)

默认值可以是更复杂的表达式,甚至是函数调用。只有在没有提供值的情况下才会使用它们。

例如,在这里我们使用 prompt 函数来提供两个默认值,但它只会在数值缺失时使用:

  1. // 默认值
  2. let [name = "Guest", surname = "Anonymous"] = ["Julius"];
  3. alert(name); // Julius (从数组里获取的)
  4. alert(surname); // Anonymous (使用了默认值)

对象解构

解构赋值对对象同样生效。

基本语法是:

  1. let {var1, var2} = {var1:…, var2…}

右边是已经存在的一个对象,我们将其解构到几个变量里。左边包含对应属性的“模式”。在这个简单的例子里,它表示所有在 {…} 中的属性名。

例如:

  1. let options = {
  2. title: "Menu",
  3. width: 100,
  4. height: 200
  5. };
  6. let {title, width, height} = options;
  7. alert(title); // Menu
  8. alert(width); // 100
  9. alert(height); // 200

属性 options.title,options.width 和 options.heght 被赋值到了对应的变量中。顺序不重要:

  1. // 改变 let {...} 变量里属性的声明顺序
  2. let {height, width, title} = { title: "Menu", height: 200, width: 100 }

左边的模式可能更复杂,用来指定属性和变量之间的映射。

如果我们想用另一个名字来接收某个属性的值,例如,options.width 属性值放在了变量 w 中,可以使用冒号来标记默认值:

  1. let options = {
  2. title: "Menu",
  3. width: 100,
  4. height: 200
  5. };
  6. // { sourceProperty: targetVariable }
  7. let {width: w, height: h, title} = options;
  8. // width -> w
  9. // height -> h
  10. // title -> title
  11. alert(title); // Menu
  12. alert(w); // 100
  13. alert(h); // 200

冒号表示“谁:去哪儿”。上面例子里,属性 width 去到了 w 中,属性 height 去到了 h 中,title 属性去到了同名的变量中。

对于潜在的缺失属性,我们可以使用“=”来设置默认值,像这样:

  1. let options = {
  2. title: "Menu"
  3. };
  4. let {width = 100, height = 200, title} = options;
  5. alert(title); // Menu
  6. alert(width); // 100
  7. alert(height); // 200

类似于数组或者函数参数,默认值可以是任何表达式甚至是函数调用。它们在值没有提供时,就会被使用。

下面的代码会询问 width,但不会询问 title。

  1. let options = {
  2. title: "Menu"
  3. };
  4. let {width = prompt("width?"), title = prompt("title?")} = options;
  5. alert(title); // Menu
  6. alert(width); // (弹窗询问时,你输入的值)

我们也可以把冒号和等号接合起来使用:

  1. let options = {
  2. title: "Menu"
  3. };
  4. let {width: w = 100, height: h = 200, title} = options;
  5. alert(title); // Menu
  6. alert(w); // 100
  7. alert(h); // 200

rest 操作符

如果对象的属性数量大于我们拥有的变量数量?我们怎么可以把对象“剩下的”属性保存在一个地方呢?

在这里使用 rest 运算符(三个点)的规范几乎是标准的,但是大多数浏览器还不支持它。

它像这样使用:

  1. let options = {
  2. title: "Menu",
  3. height: 200,
  4. width: 100
  5. };
  6. let {title, ...rest} = options;
  7. // 现在 title="Menu", rest={height: 200, width: 100}
  8. alert(rest.height); // 200
  9. alert(rest.width); // 100

⚠️没有 let 的问题

在上面的例子中,所有变量都是在赋值之前被声明的:let {…} = {…}。但是我们也可以使用已经存在的变量,但是有一个问题。

是无法正常工作的:

  1. let title, width, height;
  2. // 这一行会发生错误
  3. {title, width, height} = {title: "Menu", width: 200, height: 100};

问题是 JavaScript 将 {…} 看成是主代码流中(不是在另一个表达式中)中的一个代码块。这样的代码块可以用来对语句进行分组,如下所述:

  1. {
  2. // 一个代码块
  3. let message = "Hello";
  4. // ...
  5. alert( message );
  6. }

为了显示 它不是一个代码块,我们可以将整个任务包装在一个圆括号中 (…):

  1. let title, width, height;
  2. // 现在 OK 了
  3. ({title, width, height} = {title: "Menu", width: 200, height: 100});
  4. alert( title ); // Menu

内嵌解构

如果一个对象或者数组包含其他对象和数组,我们可以使用更加复杂的左侧模式解析更加深层的部分。

在下面的代码里, options 有一个属性 size 保存的值是一个对象;还有一个属性 items,保存的值是一个数组。赋值左侧的模式具有相同的结构:

  1. let options = {
  2. size: {
  3. width: 100,
  4. height: 200
  5. },
  6. items: ["Cake", "Donut"],
  7. extra: true // something extra that we will not destruct
  8. };
  9. // destructuring assignment on multiple lines for clarity
  10. let {
  11. size: { // 在这里给 size 赋值
  12. width,
  13. height
  14. },
  15. items: [item1, item2], // 在这里给 items 赋值
  16. title: t = "Menu" // 没有在 object 里出现(就会使用默认值)
  17. } = options;
  18. alert(title); // Menu
  19. alert(width); // 100
  20. alert(height); // 200
  21. alert(item1); // Cake
  22. alert(item2); // Donut

整个 options 对象中除了 extra 没有提到,都会复制到对应的变量里。

解构赋值 - 图1

最终,我们会得到变量 width,height,item1,item2 和使用了默认值的 title。

这经常发生在解构赋值中。我们有一个复杂的、拥有许多属性的对象,然后想从中解析出一些变量。

甚至这里发生:

  1. // 将 size 作为一个整体赋值给一个变量, 忽略剩余的
  2. let { size } = options;

智能函数参数

有时一个函数具有多个参数,其中多数参数都是可选择的。这对于用户界面来说尤其如此。想象一个创建菜单的函数,它可能接收一个宽度,一个高度,一个标题,项目列表等等。

这是一个写这样的函数的坏方法:

  1. function showMenu(title = "Untitled", width = 200, height = 100, items = []) {
  2. // ...
  3. }

在现实世界里,问题是我们怎么记住这些参数顺序呢。这一点 IDE 通常会给我们帮助,特别是在代码在非常好的组织结构下,但是仍然……另一个问题是,当大多数参数在默认情况下都是 ok 时,如何调用函数。

像这样?

  1. showMenu("My Menu", undefined, undefined, ["Item1", "Item2"])

这是丑陋的。当我们处理更多的参数时,它变得不易读。

解构来拯救!

们可以将参数作为一个对象传递,函数会立即将它们解构为变量:

  1. // 我们向函数传递一个对象
  2. let options = {
  3. title: "My menu",
  4. items: ["Item1", "Item2"]
  5. };
  6. // ...立即将它展开成变量
  7. function showMenu({title = "Untitled", width = 200, height = 100, items = []}) {
  8. // title, items – 从 options 中获取,
  9. // width, height – 使用默认值
  10. alert( `${title} ${width} ${height}` ); // My Menu 200 100
  11. alert( items ); // Item1, Item2
  12. }
  13. showMenu(options);

我们还可以使用嵌套对象和冒号映射的更复杂的解构形式:

  1. let options = {
  2. title: "My menu",
  3. items: ["Item1", "Item2"]
  4. };
  5. function showMenu({
  6. title = "Untitled",
  7. width: w = 100, // width 跑到了 w 里
  8. height: h = 200, // height 跑到了 h 里
  9. items: [item1, item2] // items 第一个元素进入 item1, 第二个元素进入 item2
  10. }) {
  11. alert( `${title} ${w} ${h}` ); // My Menu 100 200
  12. alert( item1 ); // Item1
  13. alert( item2 ); // Item2
  14. }
  15. showMenu(options);

其语法与结构赋值的语法相同:

  1. function({
  2. incomingProperty: parameterName = defaultValue
  3. ...
  4. })

请注意,这样的破坏假设 showMenu() 确实有一个参数。如果我们在默认情况下想使用所有默认值,那么我们应该指定一个空对象:

  1. showMenu({});
  2. showMenu(); // this would give an error

我们可以为整个赋值对象使用一个默认值,来解决这个问题:

  1. // 简化参数,使其更清晰
  2. function showMenu({ title = "Menu", width = 100, height = 200 } = {}) {
  3. alert( `${title} ${width} ${height}` );
  4. }
  5. showMenu(); // Menu 100 200

在上面的代码里,整个参数对象有一个可用的 {} 作为初始值,所以总有值用来解构的。

总结

  • 解构赋值立即将对象或数组映射到许多变量上。

  • 对象语法:

  1. let {prop : varName = default, ...} = object

这表示变量 prop 会进入到变量 varName 中。如果这个参数不存在的话,就使用 default 作为默认参数值。

  • 数组语法:
  1. let [item1 = default, item2, ...rest] = array

第一个数组成员进入到 item1,第二个数组成员进入到 item2,所有剩下的数组成员进入到数组 rest 里。

  • 针对更加复杂的情形,左边的结构应该与右边的结构保持一致。

(完)