JavaScript 解构赋值
在 ES6 中新增了解构赋值 destructuring 这一种特殊的赋值语法,它可以将数组(或其他可迭代对象)或普通对象「拆包」为到一系列变量中,因为有时候可能不需要一个整体的对象/数组而只需要其中部分信息。
Tip
解构操作对那些具有很多参数的函数也很奏效。
使用解构从数组和对象中提取(多个)值并赋值给(多个)变量,在赋值语句左侧使用特定的符号包含指定从数组或对象中提取的元素,对于数组使用方括号 []
,对于对象使用花括号 {}
。
- 解构数组的完整语法
let [item1 = default, item2, ...rest] = array
- 解构对象的完整语法
let {prop : varName = default, ...rest} = object
数组解构
方括号 []
表示被解构的是数组,其中包含的变量表示要将数组中的值存储的变量。赋值符号右侧的数组不需要指定提取值的索引,因为索引可以暗示出来。
Tip
解构并不意味着「破坏」原有数组,它通过将结构中的各元素复制到变量中来达到解构的目的,但数组本身是没有被修改的。
// 我们有一个存放了名字和姓氏的数组
let arr = ["Ilya", "Kantor"]
// 解构赋值
// sets firstName = arr[0]
// and surname = arr[1]
let [firstName, surname] = arr;
alert(firstName); // Ilya
alert(surname); // Kantor
// 当与 split 函数(或其他返回值是数组的函数)结合使用时,还可以对字符串进行操作,看起来就更优雅了
let [firstName, surname] = "Ilya Kantor".split(' ');
Tip
数组解构是按照从左往右的顺序依次将元素赋值给变量,可以使用逗号忽略使用部分元素;如果变量数量比数组元素少,后面的元素就会被忽略;如果希望把剩余的元素都一并收集,可以使用带有 ...
符号的前缀变量作为解构赋值最后一个参数,后续的所有元素都收集起来组成的数组赋值给这个变量。
// 忽略部分元素
const point = [10, 25, -34];
const [x, , z] = point; // 跳过了第二个元素,即忽略了 y 坐标
console.log(x, z); // 10, -34
// 使用变量 rest(用 ... 符号作标记)收集剩余元素
let [name1, name2, ...rest] = ["Julius", "Caesar", "Consul", "of the Roman Republic"];
alert(name1); // Julius
alert(name2); // Caesar
// rest 是数组
alert(rest[0]); // Consul
alert(rest[1]); // of the Roman Republic
alert(rest.length); // 2
Tip
实际上等号右侧而不仅限于数组可以是任何可迭代对象
// 字符串也可以解构为字符
let [a, b, c] = "abc"; // ["a", "b", "c"]
// 集合支持解构
let [one, two, three] = new Set([1, 2, 3]);
// 结合可迭代对象的方法 entries() 使用,循环遍历键—值对
let user = {
name: "John",
age: 30
};
for (let [key, value] of Object.entries(user)) {
alert(`${key}:${value}`); // name:John, age:30
}
Tip
赋值给等号左侧实际上不仅支持变量,还可以是任何可以被赋值的东西。
// 解构数组将元素赋值给对象的属性
let user = {};
[user.name, user.surname] = "Ilya Kantor".split(' ');
alert(user.name); // Ilya
默认值
数组解构时如果变量数量多于数组中实际元素的数量,赋值不会报错。未赋值的变量被认为是 undefined
;为了解决这个「缺陷」可以为变量设置默认值,在左侧的方括号 []
内使用 =
为变量设置默认值,它只会在这个变量未被赋值的时候才会被使用。
// 默认值
let [name = "Guest", surname = "Anonymous"] = ["Julius"];
alert(name); // Julius(来自数组的值)
alert(surname); // Anonymous(默认值被使用了)
Tip
默认值可以是更加复杂的表达式甚至可以是函数调用
// 以表达式作(prompt() 函数)作为默认值
// 由于传递的数组只有一个元素,第一个变量 name 默认值会被覆盖,只会提示输入姓氏
let [name = prompt('name?'), surname = prompt('surname?')] = ["Julius"];
alert(name); // Julius(来自数组)
alert(surname); // 你输入的值
对象解构
花括号 { }
表示被解构的是对象,其中与对象属性名称相同的变量用以存储相应的数据。赋值符号右侧的对象不用指定提取值对应的键。
// 解构对象
let gemstone = {
type: 'quartz',
color: 'rose',
karat: 21.29
};
let {type, karat} = gemstone; // 指定在解构对象时要选择的值,可以忽略部分值
console.log(type, karat); // quartz, 21.29
Tip
由于对象的属性是无序的,因此在解构时左侧的变量顺序并不重要
let options = {
title: "Menu",
width: 100,
height: 200
};
// 改变 let {...} 中元素的顺序
let {height, width, title} // title: "Menu", height: 200, width: 100
Tip
等号左侧的模式 pattern 可以更加复杂,如通过冒号 attribute:variable
指定了属性和变量之间的映射关系。
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
Warning
在对象解构时应该使用关键字 let
创建的新变量,即 let {…} = {…}
形式,如果使用已有变量就会掉进一个语法「陷阱」。因为我们需要使用 {...}
将变量包含,而 JavaScript 一般把主代码流(即不在其他表达式中)的 {...}
当做一个代码块,因此我们在前面使用关键字 let
以声明其后的是变量。为了告诉 JavaScript 这不是一个代码块,我们也可以把整个赋值表达式用括号 (...)
包起来:
let title, width, height;
// 现在就可以正常运行
({title, width, height} = {title: "Menu", width: 200, height: 100});
alert( title ); // Menu
Warning
也可以使用解构提取对象的方法(只需用于存储方法的变量为方法名即可),但是解构对象并将 方法存储到变量后,则方法无法再访问原对象,若该方法使用了 this
则无法指代原对象。
Tip
类似地,对象解构也支持剩余模式,即使用带有 ...
符号的前缀变量作为解构赋值最后一个参数,把对象剩余的属性都赋值到一个对象中,该对象被最后一个变量引用。⚠️ 一些较旧的浏览器不支持此功能,可以使用 Babel 对其进行填充,但可以在现代浏览器中使用。
let options = {
title: "Menu",
height: 200,
width: 100
};
// title = 名为 title 的属性
// rest = 存有剩余属性的对象
let {title, ...rest} = options;
// 现在 title="Menu", rest={height: 200, width: 100}
alert(rest.height); // 200
alert(rest.width); // 100
嵌套解构
如果一个对象或数组嵌套了其他的对象和数组,我们可以在等号左侧使用更复杂的模式 pattern 来提取更深层的数据。
// 对象 options 的属性 size 是另一个对象,属性 items 是另一个数组
let options = {
size: {
width: 100,
height: 200
},
items: ["Cake", "Donut"],
extra: true
};
// 为了清晰起见解构赋值语句被写成多行的形式
let {
size: { // 把 size 赋值到这里
width,
height
},
items: [item1, item2], // 把 items 赋值到这里
title = "Menu" // 在对象中不存在(使用默认值)
} = options;
// 最终得到了 width、height、item1、item2 和具有默认值的 title 变量
alert(title); // Menu
alert(width); // 100
alert(height); // 200
alert(item1); // Cake
alert(item2); // Donut
智能函数参数
但一个函数有很多参数,其中大部分的参数都是可选的(设置了默认值),可以把所有参数当作一个对象来传递,然后函数就会自动这个对象解构成多个变量,以供函数使用。
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); // Menu 100 200
// title, items 提取于 options,
// width, height 使用默认值
Tip
对于智能函数如果让所有的参数都使用默认值,应该传递一个空对象
showMenu({}); // 不错,所有值都取默认值
showMenu(); // 这样会导致错误
// 可以为示例中指定空对象来作为参数对象的默认值来解决上述的错误
function showMenu({ title = "Menu", width = 100, height = 200 } = {}) {
alert( `${title} ${width} ${height}` );
}
showMenu(); // Menu 100 200
⚠️ 推荐使用对象而非数组作为参数整体,因为数组解构是基于位置的,如果需要使用默认值,在传入数组时无法跳过部分参数的设置,而需要在使用默认值的参数的相应位置传递 undefined
作为占位符;而对象作为参数整体时,解构是基于属性名称的,是可跳过部分(解构后)选项只修改部分元素的值。
// 默认值与数组解构结合
function createSundae([scoops = 1, toppings = ['Hot Fudge']]) { … }
// 调用时仅修改第二个参数,但由于数组基于位置,需用以奇怪的方式来传递数组
// 在采用默认值就可以的数组相应位置设置 undefined
createSundae([undefined, ['Hot Fudge', 'Sprinkles', 'Caramel']]);
// 默认值与对象解构结合
function createSundae({scoops = 1, toppings = ['Hot Fudge']}) { … }
// 调用时传入的对象只需要设置修改的属性即可
createSundae({toppings: ['Hot Fudge', 'Sprinkles', 'Caramel']});