ES6中提供了一种非常方便的赋值方法:解构赋值。此外,还有组成解构的...
及其作为展开操作符时,在实际中有着很多的巧妙用法。
解构(Destructuring)是指按照一定的模式,从数组和对象中提取数据,对变量进行赋值。具体可以看后面的介绍。
展开操作符或扩展操作符、剩余操作符。
解构赋值的介绍和使用
数组或对象的解构赋值
通常,为变量赋值,只能一个个直接指定值。
let a=1;
let b=2;
let c=3;
ES6中的解构赋值写法:
let [a,b,c]=[1,2,3];
即 从数组中提取值,按照对应位置,对变量赋值。
这种写法属于“模式匹配”,需要保证等号两边的模式相同。只要模式结构能够匹配上,左边的变量就可以赋值成功。
因此对于嵌套数字、对象等结构,都可以使用解构赋值。
let [foo, [[bar], baz]] = [1, [[2], 3]];
foo // 1
bar // 2
baz // 3
let [ , , third] = ["foo", "bar", "baz"];
third // "baz"
let { foo, bar } = { foo: 'aaa', bar: 'bbb' };
let obj = {
p: [
'Hello',
{ y: 'World' }
]
};
let { p: [x, { y }] } = obj;
只要对应的模式能够匹配,就可以赋值成功。数组的元素按次序排列,变量的取值由它的位置决定;对象通过属性取值,变量需要与属性同名,才能取到正确的值。
let { bar, foo } = { foo: 'aaa', bar: 'bbb' };
foo // "aaa"
bar // "bbb"
let { baz } = { foo: 'aaa', bar: 'bbb' };
baz // undefined
如果解构不成功,变量的值就等于undefined
。
不完全解构是指等号左边的模式只匹配右边结构的一部分。
let arr1 = [1, 2, 3];
let [x, y] = arr1;
x // 1
y // 2
let arr2 = [1, [2, 3], 4];
let [a, [b], d] = arr2;
a // 1
b // 2
d // 4
对于数组解构,等号右边可以数组或Set(严格将只要就要Iterator接口就可以),否则会报错;对于对象,等号右侧必须为对象。
let [x, y, z] = new Set(['a', 'b', 'c']);
对象的解构赋值可以取到继承的属性。
解构中...
的使用
let [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4]
let [x, y, ...z] = ['a'];
x // "a"
y // undefined
z // []
数组作为特殊的对象,进行对象属性的解构。
由于数组本质是特殊的对象,因此:
let arr = [1, 2, 3];
let {0 : first, [arr.length - 1] : last} = arr;
first // 1
last // 3
对象解构赋值时的变量名和嵌套
不同于属性名的变量
如果对象的属性名和变量名不一致,想要解构成功,需要写成如下方式:
let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"
let obj = { first: 'hello', last: 'world' };
let { first: f, last: l } = obj;
f // 'hello'
l // 'world'
也就是在解构赋值的左侧,也以key:value
的形式,对象解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。赋值的不是属性,而是属性名对应的变量。
let { foo, bar } = { foo: 'aaa', bar: 'bbb' };
// 实际为:
let { foo: foo, bar: bar } = { foo: 'aaa', bar: 'bbb' };
对象的简写形式{ foo, bar }
,实际表示的是{ foo: foo, bar: bar }
,也是ES6提供的扩展。
let foo = 'a';
let bar = 'b';
let obj={ foo, bar };
console.log(obj); // {foo: 'a', bar: 'b'}
嵌套对象解构时的模式
嵌套结构的对象进行解构时,需要注意用于匹配嵌套的模式。
let obj = {
p: [
'Hello',
{ y: 'World' }
]
};
let { p: [x, { y }] } = obj;
x // "Hello"
y // "World"
上面的解构中,p
是模式,而不是变量。如果想要p
也作为变量赋值,应该使用如下的方式:
let obj = {
p: [
'Hello',
{ y: 'World' }
]
};
let { p, p: [x, { y }] } = obj;
x // "Hello"
y // "World"
p // ["Hello", {y: "World"}]
解构时指定默认值
解构赋值时允许指定默认值。
- 数组解构时的默认值
let [foo = true] = [];
foo // true
let [x, y = 'b'] = ['a']; // x='a', y='b'
let [x, y = 'b'] = ['a', undefined]; // x='a', y='b'
ES6 内部使用严格相等运算符(===
)判断是否为undefined
,判断一个变量是否有值。也就是,只有真正的没有该值或值为undefined
,默认值才会生效。
let [x = 1] = [null];
x // null
- 对象的解构指定默认值
let {x = 3} = {};
x // 3
let {x, y = 5} = {x: 1};
x // 1
y // 5
let {x: y = 3} = {};
y // 3
let {x: y = 3} = {x: 5};
y // 5
let { message: msg = 'Something went wrong' } = {};
msg // "Something went wrong"
- 解构时默认值为表达式时的惰性求值
如果默认值是一个表达式,那么这个表达式是惰性求值的,即只有在用到的时候,才会求值。
function f() {
console.log('aaa');
}
let [x = f()] = [1];
x
能取到值,即不会获取默认值,函数f
也不会执行。此代码等价于:
let x;
if ([1][0] === undefined) {
x = f();
} else {
x = [1][0];
}
- 默认值如果为变量,则必须已经声明
let [x = y, y = 1] = []; // ReferenceError: y is not defined
函数参数的解构赋值
函数参数使用解构
作为函数的参数使用解构赋值。
function add([x, y]){
return x + y;
}
add([1, 2]); // 3
function add({x, y}){
console.log(x , y);
}
add({x:1, y:2}); // 1 2
传入的参数被解构为x、y变量在函数内部使用。
比如,使用map时,解构获取元素为变量:
[[1, 2], [3, 4], [5, 6]].map(([a, b]) => a + b);
// [3, 7, 11]
函数参数的解构使用默认值
function move({x = 0, y = 0} = {}) {
return [x, y];
}
move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, 0]
move({}); // [0, 0]
move(); // [0, 0]
...展开运算符或剩余操作符
...
有两种作用:
展开运算符
有时也被称为“扩展操作符”。
- 展开运算符(
spread operator
),用于把array
和object
结构展开;
let a = [1,2,3];
let b = [0, ...a, 4]; // [0,1,2,3,4]
let obj = { a: 1, b: 2 };
let obj2 = { ...obj, c: 3 }; // { a:1, b:2, c:3 }
let obj3 = { ...obj, a: 3 }; // { a:3, b:2 }
作为展开操作符展开对象时,会出现属性覆盖的问题,这一点需要注意。利用这一点,也可以处理参数的默认值覆盖。
function operator(opt){
opt={prop1:value1,prop2:value2,...opt}
// 类似
// opt=Object.assign({},{prop1:value1,prop2:value2},opt);
}
不同通常在参数中使用解构赋值时的默认值,比这中方式更简洁。
展开对象时仅获取对象自身的可枚举属性,也就是会丢失原型链上的属性或方法。
比如,通过class
类创建的对象实例,在展开时就会丢失其方法。
class A {
a = 1;
m() {
}
}
let a=new A();
let b={...a};
b.p;
b.m(); // TypeError: b.m is not a function
class定义的实例方法位于原型对象上:
a // console中输出a A a: 12 [[Prototype]]: Object constructor: class A m: ƒ m() [[Prototype]]: Object ...
展开操作符展开复制时,使用的是浅拷贝。
剩余操作符
- 剩余操作符(
rest operator
),是解构的一种,意思就是把剩余的项放到一个数组或对象里面赋值给变量,除了变量赋值,也常用在函数的剩余参数中。
剩余操作符符不仅针对数组,也可以用于对象。
let a = [1,2,3];
let [b, ...c] = a;
b; // 1
c; // [2,3]
// 或
let a = [1,2,3];
let [b, ...[c,d,e]] = a;
b; // 1
c; // 2
d; // 3
e; // undefined
函数的剩余参数,用于获取函数的多余参数,这样就不需要使用arguments
对象。
function test(a, ...rest){
console.log(a); // 1
console.log(rest); // [2,3]
}
test(1,2,3)
剩余操作符应用于对象时:
let obj = { a: 1, b: 2 };
let {...obj2} = obj;
obj2; // {a: 1, b: 2}
解构赋值的实际使用
交换变量的值
简洁、易读、语义清晰的交换变量值。
let x=1;
let y=2;
[x,y] = [y,x];
从函数返回值中获取多个变量
函数只能返回一个值,想返回多个值,只能将它们放在数组或对象里返回。使用解构赋值,可以方便的取出来。
// 返回数组
function example() {
return [1, 2, 3];
}
let [a, b, c] = example();
// 返回对象
function example() {
return {
foo: 1,
bar: 2
};
}
let { foo, bar } = example();
函数参数中方便地指定变量名
// 参数是一组有次序的值
function f([x, y, z]) { ... }
f([1, 2, 3]);
// 参数是一组无次序的值
function f({x, y, z}) { ... }
f({z: 3, y: 2, x: 1});
非常方便地提取JSON对象数据(解构对象)
let jsonData = {
id: 42,
status: "OK",
data: [867, 5309]
};
let { id, status, data: number } = jsonData;
console.log(id, status, number);
// 42, "OK", [867, 5309]
防止了一个属性一个属性的获取赋值:
let id = jsonData.id;
let number = jsonData.data;
// ...
函数参数的默认值
jQuery.ajax = function (url, {
async = true,
beforeSend = function () {},
cache = true,
complete = function () {},
crossDomain = false,
global = true,
// ... more config
} = {}) {
// ... do stuff
};
指定参数的默认值,避免在函数体内部再写var foo = config.foo || 'default foo';
这样的默认值处理相关的语句。
遍历Map结构
方便的获取键名和键值:
const map = new Map();
map.set('first', 'hello');
map.set('second', 'world');
for (let [key, value] of map) {
console.log(key + " is " + value);
}
只获取键名,或者只获取键值:
// 获取键名
for (let [key] of map) {
// ...
}
// 获取键值
for (let [,value] of map) {
// ...
}
输入模块的指定方法
非常常见的方式,在引入一个模块时,通常要指定输入的方法,通过解构赋值非常清晰简洁。现在开发中使用最普遍的场景。
const { SourceMapConsumer, SourceNode } = require("source-map");
扩展操作符的实际使用
将字符串每个字符展开为数组
let str = 'hello';
let arr = [...str];
// let arr = str.split('');
console.log(arr); // ['h', 'e', 'l', 'l', 'o']
将类数组对象转换为数组
js中,类数组对象是拥有一个 length
属性和若干索引属性的任意对象。
类数组对象非常常见,比如:
NodeList
:document.querySelectorAll()
返回的对象;HTMLCollection
:document.getElementsByTagName()
、document.getElementsByClassName()
等方法返回的对象;- 字符串;
Arguments
:函数里的参数对象;
类数组对象不是数组,想要使用数组的方法,需要将其转换为数组,或改变数组的原始方法的this指向类数组对象并执行。
- 利用
apply
方法通过Array
构造函数将类数组对象转换为数组
let nodeList = document.querySelectorAll('div');
console.log(nodeList instanceof NodeList); // true
let arr = Array.apply(null, nodeList);
console.log(arr instanceof Array); // true
Array.from
将类数组对象转换为数组
let arr3 = Array.from(nodeList);
console.log(arr3 instanceof Array); // true
- 通过改变this指向执行数组方法
let arr2 = [].slice.call(nodeList);
// let arr3 = Array.prototype.slice.call(nodeList);
// let arr4 = [].slice.apply(nodeList);
// let arr5 = Array.prototype.slice.apply(nodeList);
console.log(arr2 instanceof Array); // true
或
let arr2 = Array.prototype.slice.call(nodeList,2,5);
console.log(arr2);
let arr3 = Array.prototype.map.call(nodeList,function(node){
console.log(node);
});
- 展开操作符将类数组对象转换为数组
let nodeList = document.querySelectorAll('div')
let arr = [...nodeList]
console.log(arr instanceof Array) // true
拷贝数组和对象
拷贝数组通常可以使用concat()
、slice()
、new Array
、Array.from()
等实现。
let arr = [1, 2, 1, 0];
let arr2 = arr.concat();
let arr3 = arr.slice();
console.log(arr===arr2); // false
console.log(arr2===arr3); // false
console.log(arr===arr3); // false
通过展开操作,实现对数组的拷贝(新建数组)
let arr = [1, 2, 1, 0];
let arr2 = [...arr];
console.log(arr===arr2); // false
对象的拷贝,一般的做法是使用Object.assign({}, obj)
。
let obj = { foo: 'xx', bar: 12};
let obj2 = Object.assign({}, obj);
展开操作符展开拷贝对象。
let obj = { foo: 'xx', bar: 12};
let obj2 = {...obj};
合并数组或对象
concat
合并数组
arr1.concat(arr2)
合并数组会返回一个新的数组,不会改变原数组。
let arr1 = [1, 3, 5];
let arr2 = [2, 4, 6];
let arr3 = arr1.concat( arr2 );
console.log(arr3); // [1, 3, 5, 2, 4, 6]
- 使用展开操作符合并数组
let arr1 = [1, 3, 5];
let arr2 = [2, 4, 6];
let arr3 = [...arr1, ...arr2];
console.log(arr3); // [1, 3, 5, 2, 4, 6]
替代合并对象的Object.assign({}, obj1, obj2)
方法:
let obj1 = { foo: 'xx' };
let obj2 = { bar: 12 };
let obj3 = Object.assign({}, obj1, obj2);
// 合并两个对象
let obj4 = {...obj1, ...obj2};
将数组或类数组对象展开为函数的参数列表
通常函数都是通过一系列的参数列表接受传递的参数,如果想要将数组项应用到函数中,通常需要借助apply
方法。
let arr = [1, 3, 5];
function fn(a, b, c) { };
fn.apply(null, arr);
如果使用扩展操作符,将会很简洁:
let arr = [1, 3, 5];
function fn(a, b, c) { };
fn(...arr);
这对于处理很多数组不存在的方法很有帮助。比如数组没有默认的求最大值、最小值等的方法,但是Math对象中有提供。因此,实际处理就会变成如下的形式:
let arr = [1,3,5,6,0,12,1];
Math.min.apply(null, arr); // 0
Math.max.apply(null, arr); // 12
// 使用扩展操作符
Math.min(...arr); // 0
Math.max(...arr); // 12
这样,可以扩展很多数组能使用的方法。
数组通过
reduce
方法获取最大或最小值的实现:arr.reduce((acc, cur) => Math.max(acc, cur)); // 12 arr.reduce((acc, cur) => Math.min(acc, cur)); // 0
作为函数的剩余参数替代arguments
前面已经介绍了...
作为函数剩余参数的使用。
在只有剩余参数时,函数接受传递的任意多个参数:
function totalSum(...arr){
return arr.reduce((acc, cur) => acc + cur);
}
console.log( totalSum(1, 5) ); // 6
console.log( doSum(1, 3, 5) ); // 9
console.log( doSum(1, 3, 5, 9) ); // 18
向数组中添加项或向对象添加属性
数组原生提供了几种向数组中添加数据项的方法,如:
let arr = [10,9];
// 向头部添加
arr.unshift(1, 2);
console.log(arr); // [1, 2, 10, 9]
// 向尾部添加
arr.push(6, 7);
console.log(arr); // [1,2, 10, 9, 6, 7]
// 在任意位置添加
arr.splice(2, 0, 3, 4);
console.log(arr); // [1, 2, 3, 4, 10, 9, 6, 7]
使用扩展操作符:
let arr = [8,7];
arr = [...arr, 5];
arr = [1, 2, ...arr, 6];
arr = [0, ...arr];
console.log(arr); // [0, 1, 2, 8, 7, 5, 6]
对象添加属性的方式直接.propName
赋值即可。
let obj = { foo: 'xx', bar: 12};
obj.a = 'aaa';
console.log(obj); // {foo: 'xx', bar: 12, a: 'aaa'}
使用扩展操作符(注意属性的覆盖或重写):
let obj = { foo: 'xx', bar: 12};
obj = {...obj, a:'aaa'};
console.log(obj); // {foo: 'xx', bar: 12, a: 'aaa'}
使用扩展操作符添加数组项或对象属性的处理,本质上是新建了一个数组或对象。相对来说,还是推荐原来的方式。
利用向对象添加属性的方式,可以通过扩展操作符,向对象中添加Getter/Setter的处理【本质上也是新建对象】
为js对象的属性添加Getter/Setter方法的标准方式有如下两种:
let obj = { foo: 'xx', get bar(){ return 12; }, set bar(value) { console.log(value); } }; // 或者 let obj = { foo: 'xx' }; Object.defineProperty(obj,'bar',{ get:function(){ return 12; }, set:function(value){ console.log(`Setter:${value}`); } // 或 // get() {return 12;}, // set(value) { console.log(`Setter:${value}`); } });
使用扩展操作符,在展开对象时,添加Getter/Setter:
let obj = { foo: 'xx' }; obj = { ...obj, get bar(){ return 1; } }
参考
- ECMAScript 6 入门-变量的解构赋值,感谢阮一峰老师。
- 让人爱不释手的 JS 扩展操作符