JavaScript 中最常用到的两种数据结构是 Object 和 Array。
- 对象允许我们创建一个按键存储数据项的实体。
- 数组允许我们将数据项收集到有序列表中。
不过,当我们将这些数据传递给一个函数时,可能不需要作为一个整体的对象/数组传递,可能需要单独的部分。
解构赋值是一种特殊的语法,它允许我们将数组或对象“解包”成一堆变量,因为有时这样更方便。
解构也特别适用于含有大量参数、默认值等的复杂函数。很快我们就会看到。
数组结构
这个例子展示了一个数组是如何解构为变量的:
// we have an array with the name and surname
let arr = ["John", "Smith"]
// destructuring assignment
// sets firstName = arr[0]
// and surname = arr[1]
let [firstName, surname] = arr;
alert(firstName); // John
alert(surname); // Smith
现在可以就可以使用这些变量了,而不必访问数组的元素。
配合 split 或者其他返回数组的函数使用特别方便:
let [firstName, surname] = "John Smith".split(' ');
alert(firstName); // John
alert(surname); // Smith
如你所见,语法很简单。不过有几个特别的细节。让我们看更多的例子,以更好地理解它。
解构不会破坏原数据结构
之所以称之为“解构赋值”,是因为把数据项“解构”后复制到变量里。但是数组自身未改变。
它只是如下写法的快捷方式:
// let [firstName, surname] = arr;
let firstName = arr[0];
let surname = arr[1];
使用逗号忽略元素
数组里不想要的元素可以用一个多余的逗号忽略掉:
// second element is not needed
let [firstName, , 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]);
上面的代码可以运行,是因为解构赋值遍历了等号右侧的值。它在底层为等号右侧的值调用了 for...of 然后赋值。
左侧赋值
在左侧可以使用任何可赋值的变量。
例如,可以是对象的属性:
let user = {};
[user.name, user.surname] = "John Smith".split(' ');
alert(user.name); // John
alert(user.surname); // Smith
配合 .entries() 遍历
我们可以用解构赋值配合它来实现一个对象键值对的遍历。
let user = {
name: "John",
age: 30
};
// loop over keys-and-values
for (let [key, value] of Object.entries(user)) {
alert(`${key}:${value}`); // name:John, then age:30
}
对于 Map 的遍历代码类似,它更简单一些,因为它是可迭代的:
let user = new Map();
user.set("name", "John");
user.set("age", "30");
// Map iterates as [key, value] pairs, very convenient for destructuring
for (let [key, value] of user) {
alert(`${key}:${value}`); // name:John, then age:30
}
交换变量的技巧
使用解构赋值交换两个变量的值有一个众所周知的技巧:
let guest = "Jane";
let admin = "Pete";
// Let's swap the values: make guest=Pete, admin=Jane
[guest, admin] = [admin, guest];
alert(`${guest} ${admin}`); // Pete Jane (successfully swapped!)
在这里,我们创建一个包含两个变量的临时数组,并交换它们顺序对其进行解构。
我们可以用这种方式交换多个变量。
剩余参数 '...'
通常,如果数组比左边的变量列表长,那么“额外”的元素就会被忽略。
例如,这里只有两个元素赋值,剩余的被忽略。
let [name1, name2] = ["Julius", "Caesar", "Consul", "of the Roman Republic"];
alert(name1); // Julius
alert(name2); // Caesar
// Further items aren't assigned anywhere
如果我们也想收集剩下的元素,这时可以用三个点 "..." 多加一个参数。
let [name1, name2, ...rest] = ["Julius", "Caesar", "Consul", "of the Roman Republic"];
// rest is array of items, starting from the 3rd one
alert(rest[0]); // Consul
alert(rest[1]); // of the Roman Republic
alert(rest.length); // 2
rest 的值是由剩余元素组成的一个数组。
我们可以用任意其他的变量名来替代 rest,只要它前面有三个点并且放在解构赋值的最后即可。
let [name1, name2, ...titles] = ["Julius", "Caesar", "Consul", "of the Roman Republic"];
// now titles = ["Consul", "of the Roman Republic"]
默认值
如果数组比左边的变量列表短,不会有报错。缺少的值被视为 undefined :
let [firstName, surname] = [];
alert(firstName); // undefined
alert(surname); // undefined
如果我们想要一个默认值来代替缺失的值,我们可以使用 =:
// default values
let [name = "Guest", surname = "Anonymous"] = ["Julius"];
alert(name); // Julius (from array)
alert(surname); // Anonymous (default used)
默认值可以是更复杂的表达式,甚至是函数调用。只有在未提供值的情况下,才会执行它们。
例如,这里我们使用了 prompt 函数来指定两个默认值:
// runs only prompt for surname
let [name = prompt('name?'), surname = prompt('surname?')] = ["Julius"];
alert(name); // Julius (from array)
alert(surname); // whatever prompt gets
请注意:prompt 只会为缺失的值调用(surname)。
对象解构
解构赋值也可以用于对象。
基本语法如下:
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.height 属性被分配给相应的变量。
变量名的顺序无关紧要,这样也可以:
// changed the order in 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
和数组或函数参数类似,默认值可以是任意的表达式或者函数调用。它们会在值未提供时执行。
下面的代码会调用 prompt 为 width 赋值,但对于 title 则不会。
let options = {
title: "Menu"
};
let {width = prompt("width?"), title = prompt("title?")} = options;
alert(title); // Menu
alert(width); // (whatever the result of prompt is)
我们也可以同时使用冒号和等号:
let options = {
title: "Menu"
};
let {width: w = 100, height: h = 200, title} = options;
alert(title); // Menu
alert(w); // 100
alert(h); // 200
如果一个复杂的对象有很多属性,我们可以只提取所需的属性:
let options = {
title: "Menu",
width: 100,
height: 200
};
// only extract title as a variable
let { title } = options;
alert(title); // Menu
剩余模式 "..."
如果对象的属性比我们的变量多会怎样?我们可以只接收部分然后把剩下的赋值到别处吗?
我们可以用剩余模式,就像应用到数组那样。这个功能在一些旧版浏览器上不支持(例如 IE, 可以用 Babel 来兼容),但是在现代浏览器上支持情况良好。
例如:
let options = {
title: "Menu",
height: 200,
width: 100
};
// title = property named title
// rest = object with the rest of properties
let {title, ...rest} = options;
// now title="Menu", rest={height: 200, width: 100}
alert(rest.height); // 200
alert(rest.width); // 100
如果不用 let 时的坑
上面的例子中我们都是这样赋值的:let {...} = {...}。当然了,也可以不写 let 使用已有的变量名。不过有一个坑需要注意。
这样不行:
let title, width, height;
// error in this line
{title, width, height} = {title: "Menu", width: 200, height: 100};
问题在于 JavaScript 会把 {...} 视为一个代码块。在 JavaScript 中代码块用于把语句进行分组,如下:
{
// a code block
let message = "Hello";
// ...
alert( message );
}
所以在这里 JavaScript 会认为我们写了一个代码块,那就是为什么代码报错了。我们想要的是解构。
要告知 JavaScript 它不是一个代码块,我们需要把这个表达式包裹在一对括号里(...):
let title, width, height;
// okay now
({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
};
// destructuring assignment split in multiple lines for clarity
let {
size: { // put size here
width,
height
},
items: [item1, item2], // assign items here
title = "Menu" // not present in the object (default value is used)
} = options;
alert(title); // Menu
alert(width); // 100
alert(height); // 200
alert(item1); // Cake
alert(item2); // Donut
options 对象的除了 extra 之外所有属性都赋值到左侧对应的变量里:
最后,width, height, item1 和 item2 以及 title 都有值了。
注意没有为 size 和 items 设置变量,因为我们提取了它们的内容。
智能函数参数
有时候一个函数有很多参数,大部分参数是可选的。对于用户界面尤其如此。设想有个函数用于创建菜单。它需要设置宽度,高度,标题,数据列表等等。
这是一种不好的写法:
function showMenu(title = "Untitled", width = 200, height = 100, items = []) {
// ...
}
在现实生活中,问题是怎么记住这些参数的顺序。一般来说 IDE 会提示,特别是代码注释规范时,不过... 另外一个问题是当大多数参数采用默认值即可时怎么调用函数。
像这样?
// undefined where default values are fine
showMenu("My Menu", undefined, undefined, ["Item1", "Item2"])
那样很丑陋。而且随着参数的增多可读性很差。
解构赋值这个救星来了!
我们可以把参数作为一个对象,函数会把它们解构为变量:
// we pass object to function
let options = {
title: "My menu",
items: ["Item1", "Item2"]
};
// ...and it immediately expands it to variables
function showMenu({title = "Untitled", width = 200, height = 100, items = []}) {
// title, items – taken from options,
// width, height – defaults used
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 goes to w
height: h = 200, // height goes to h
items: [item1, item2] // items first element goes to item1, second to item2
}) {
alert( `${title} ${w} ${h}` ); // My Menu 100 200
alert( item1 ); // Item1
alert( item2 ); // Item2
}
showMenu(options);
完整的语法和解构赋值是一样的:
function({
incomingProperty: varName = defaultValue
...
})
对于参数组成的一个对象,会有一个名为 varName 的变量对应于 incomingProperty 属性,它的默认值是 defaultValue。
注意这样的写法会假设 showMenu() 调用时传入了一个参数。如果想要所有的值都是默认值,需要传入一个空对象:
showMenu({}); // ok, all values are default
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, ...rest} = object
prop 属性值会赋值到 varName 变量里,如果没有这个属性,则使用 default 值。
没有映射关系的属性会被拷贝到 rest 对象里。
- 数组解构赋值语法:
let [item1 = default, item2, ...rest] = array
第一个元素赋值到 item1;第二个赋值到 item2,剩余的构成了 rest 数组。
- 从嵌套数组/对象里抽取数据也是可以的,此时左侧数据结构需要和右侧的保持一致。