目录
- 数组的解构赋值
- 对象的解构赋值
- 字符串的解构赋值
- 数值和布尔值的解构赋值
- 函数参数的解构赋值
- 圆括号问题
- 参考 | 推荐
数组的解构赋值
基本用法
ES6 允许按照一定的模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。
js
let [a, b, c] = [1, 2, 3];
上面的代码表示,可以从数组中提取值,按照对应的位置,对变量赋值。
本质上,这种写法属于"模式匹配",只要等号两边的模式相同,左边的变量就会被赋予对应的值。例如:
js
let [foo1, [foo2, [foo3]] = [1, [2, [3]];
foo1 // 1
foo2 // 2
foo3 // 3
let [ , , third] = ["foo", "bar","baz"];
third // "baz"
let [x, , y] = [1, 2, 3]
x // 1
y // 3
let [head, ...tail] = [1, 2, 3, 4]
head // 1
tail // [2, 3, 4]
let [x, y, ...z] = ['a']
x // "a"
y // undefined
z // []
解构不成功,即等号左边的变量匹配不到等号右边的值,变量的值就等于undefined
js
let [foo1] = [];
foo1 // undefined
let [bar, foo2] = [1];
foo2 // undefined
可以看出,上面两个都属于解构不成功,foo1
,foo2
都为undefined
还有一种情况是不完全解构,即等号左边的模式,只匹配一部分的等号右边的数组。在这种情况下,解构依然可以成功。
js
let [x, y] = [1, 2, 3];
x // 1
y // 2
可以看到等号右边的3并没有与之配对的变量,但解构依然成功,这就是不完全解构。
如果等号右边不是数组(或者严格地说,不是可遍历的解构),那么将会报错
js
let [foo1] = 1;
let [foo2] = false;
let [foo3] = NaN;
let [foo4] = undefined;
let [foo5] = null;
let [foo6] = {};
上面的语句都会报错,因为等号右边的值,要么转为对象以后不具备Iterator接口(前五个表达式), 要么本身就不具备Iterator接口。
其实,只要某种数据结构具有Iterator 接口, 都可以采用数组形式的解构赋值。
js
let [x, y, z] = new Set(['a', 'b', 'c']);
x // 'a'
js
function* fibs() {
let a = 0;
let b = 1;
while(true) {
yield a;
[a, b] = [b, a + b];
}
}
let [first, second, third, fourth, fifth, sixth] = fibs();
sixth // 5
fibs
是一个Generator函数,原生具有Iterator接口,解构赋值会依次从这个接口获取值。
默认值
解构赋值允许指定默认值
js
let [foo1 = 1] = [];
let [foo2 = 2] = ["two"];
let [foo3 = 3] = [undefined]
let [foo4 = 4] = [null]
foo1 // 1
foo2 // two
foo3 // 3
foo4 // null
ES6内部使用严格相等运算符(===
),判断一个位置是否有值。所以,只有当一个数组成员严格等于undefined
,默认值才会生效。
默认值也可以是表达式,这个表达式是惰性求值的,即只有在用到的时候,才会求值。
js
function f() {
console.log('hello');
}
let [x = f()] = [];
x // 'hello'
默认值也可以引用解构赋值中的其他变量,但该变量必须要先声明
js
let [foo1 = 1, bar1 = foo1] = []; // foo1=1;bar1=1
let [foo2 = 1, bar2 = foo2] = [2]; // foo2=2,bar2=2
let [foo3 = 1, bar3 = foo3] = [1,2]; // foo3=1,bar3=2
let [foo4 = bar4, bar4 = 1] = []; // ReferenceError : bar4 is not defined
最后一个表达式之所以会报错,是因为在给foo4
赋值之前,bar4
还没有声明。
对象的解构赋值
基本用法
变量的解构是用{}
包括的,而数组是用[]
解构
js
let { foo, bar } = {foo : 'aaa', bar: 'bbb'}
foo // 'aaa'
bar // 'bbb'
对象的解构和数组还有一个明显的不同,那就是数组的元素是按次序排列,变量的取值由它的位置决定;而对象的解构没有次序,变量必须与属性同名,才能取到正确的值。
js
let {bar, foo} = {foo : 'aaa', bar: 'bbb'}
foo //'aaa'
bar // 'bbb'
let {baz} = {foo : 'aaa'}
baz // undefined
可以发现,第一个例子中,即使bar
和foo
的次序与等号右边的不一致,也能取到同名对应的值;而第二个例子中,即使baz
与foo
的次序对应,但因为等号右边没有与baz
同名的属性,所以为undefined
。
对象解构失败,其变量的值也等于undefined
js
let {foo} = { bar : 'aaa' }
foo // undefined
对象解构赋值, 可以很方便的把现有对象的方法赋值给一个变量。
js
let {log, sin, cos} = Math;
const {log} = console;
log{'hello'} // hello
上面的第一个代码将Math
对象的对数、正弦、余弦三个方法,赋值到对应的变量上,使用起来更方便,例二也是如此。将console
对象中的log
方法赋值给log
对象。
如果变量名与属性名想要不一致,那就必须写成下面这样。
js
let {foo : a} = {foo : '1', bar: '2'};
a // '1'
其实对象的解构赋值就是下面形式的简写。
js
let { foo : foo, bar : bar } = { foo : 'aaa', bar : 'bbb'}
也就是说,对象的解构赋值的内部机制,是先找到同名属性,然后再赋值给对应的变量。真正被赋值的是冒号后面的,前面的只是用来找到属性的key。
js
let { foo : baz } = { foo : 'aaa', baz : 'bbb' }
foo // foo is not found
baz // 'aaa'
与数组一样,解构也可以用于嵌套解构的对象。
js
let obj = {
p : [
'hello',
{y : 'world'}
]
}
let { p : [x, { y }] } = obj;
//x 'hello'
//y 'world'
注意, 这时p
是模式,不是变量,p
只是用来匹配的key,因此不会被赋值。如果p
也要作为变量赋值,可以写成下面这样。
js
let obj = {
p : [
'hello',
{y : 'world'}
]
}
let { p, p : [x, { y }] } = obj;
// p ['hello', {'world'}]
// x 'hello'
// y 'world'
如果解构模式是嵌套的对象,而且子对象所在的父属性不存在,那么将会报错
js
// 报错
let { foo : {bar}} = { bar: 'bar' }
因为只有子属性bar
,没有父属性foo
。在解构时,foo
为undefined
,再取foo
的子属性就会报错。
对象的解构赋值也可以取到继承的属性。
js
const obj1 = {};
const obj2 = { foo: 'bar' };
Object.setPrototypeOf( obj1, obj2 );
const { foo } = obj1;
foo // 'bar'
obj1
的原型对象为obj2
,而obj2
有一个foo
属性,在解构时,foo
变量会顺着原型链依次查找是否有同名的属性,若有则赋值给foo
。
默认值
对象解构也可以指定默认值
js
var { x = 3 } = {};
x // 3
var { x : y = 2 } = {}
y // 2
var { x : y = 2 } = {x = 1};
y // 1
var { x : y = 'hello'} = {};
y // 'hello'
对象的默认值生效的条件跟数组的一样,对象的属性值严格等于undefined
js
var { x = 3 } = { x : undefined }
x // 3
var { y = 2 } = { y : null }
y // null
注意点
(1) 如果将一个已经声明的变量用于解构赋值, 需要注意
js
// 错误写法
let x;
{x} = {x : 1}
// SyntaxError: syntax error
上面的代码会报错, 因为JavaScript 引擎会将 {x} 理解成一个代码块, 从而发生了语法的错误。 只有不将大括号写在行首,避免JavaScript将其解释为代码块,才能解决这个问题。后面的圆括号也能解决这个问题。例如:
js
// 正确的写法
({x} = {x : 1})
上面代码将整个解构赋值语句,放在一个圆括号里面,就可以正确执行,具体放在下文详细讲解。
(2) 解构赋值允许等号左边的模式之中,不放置任何变量名。因此,可以写出非常古怪的赋值表达式。
js
([] = [ true, false]);
({} = 'abc');
({} = []);
上面的表达式虽然没什么意义,但在语法上是合法的,可以执行。
(3) 由于数组本质是特殊的对象, 因此可以对数组进行对象属性的解构。
js
let arr = [1, 2, 3];
let { 0 : first, [arr.length - 1] : last, length : len} = arr;
first // 1
last // 3
len // 3
上面对arr数组进行解构,但我们用的是{}
,把数组当成一个对象来解构的,所以数组的下标对应就是相应的属性,而数组对象方法也可以解构出来,比如length
。
字符串的解构赋值
字符串也可以解构赋值。这是因为字符串被转换成了一个类似数组的对象。
js
const [a, b, c, d, e] = 'hello';
a // 'h'
b // 'e'
c // 'l'
d // 'l'
e // 'o'
类似数组对象有length
属性,也可以对字符串对象的这个属性进行解构赋值。
js
let {length : len} = 'hello';
len // 5
数值与布尔值的解构赋值
解构赋值时,如果等号右边是数值和布尔值,则会先转为对象。
js
let { toString : n } = 123;
s === Number.prototype.toString // true
let { toString : b } = true;
b === Boolean.prototype.toString // true
解构赋值的规则就是, 只要等号右边的值不是对象或数组, 就先将其转为对象。 由于undefined
和 null
无法转为对象, 所以对它们进行解构赋值,都会报错。
js
let { prop: x } = undefined; //TypeError
let { prop: y } = null; //TypeError
函数参数的解构赋值
函数参数也可以解构赋值。
js
function add ([x , y]) {
return x + y;
}
add([1, 2]);
上面的代码中,函数add
的参数表面上是一个数组,但在传入参数的那一刻,数组参数便被解构成变量x
和y
,可以直接在函数中使用x
或y
。
函数参数的解构也可以使用默认值。
js
function f({x = 0, y = 0} = {}) {
return [x, y];
}
f({x : 3, y : 8}); //[3, 8]
f({x : 3}); //[3, 0]
f({}); //[0, 0]
f(); //[0, 0]
function f({x = 0, y = 0} = {})
的参数为什么要= {}
呢
js
function f({x = 0, y = 0}) {
return [x, y];
}
f({x : 3, y : 8}); //[3, 8]
f({x : 3}); //[3, 0];
f({}); //[0, 0];
f(); // 报错
我们发现,直接调用f()
会报错,因为不传参数时,函数内部收到的参数是 undefined
对 undefined
进行解构赋值会抛出类型错误。
圆括号问题
首先我们先来介绍一下模式, 在JavaScript解构赋值中,模式(Pattern)指的是等号左侧的变量声明结构,主要分为两种类型:对象模式 数组模式
上面我们知道,如果{}
的{
位于开头,就会被JavaScript
解析成代码块,会发生语法错误。而圆括号便能解决这个问题。问题是,如果模式中出现圆括号怎么处理。
ES6 的规则是, 只要有可能导致解构的歧义,就不得使用圆括号。
不能使用圆括号的情况
以下三种解构赋值不得使用圆括号。
(1) 变量声明语句
js
let [(a)] = [1];
let {x : (c)} = {};
let ({x : c}) = {};
let {(x : c)} = {};
let ((x): c) = {};
let { o : ({p : p}) } = {o: { p : 2 } };
上面都会报错,因为它们都是变量声明语句,模式不能使用圆括号。
(2) 函数参数
函数参数也属于变量声明,因此不能带有圆括号
js
function f([(z)]) {
}
(3) 赋值语句
js
({ p: a }) = { p : 42 };
([a]) = [5];
上面代码会报错,因为它将圆括号包裹在了模式之中,导致语法错误。
可以使用圆括号的情况
可以使用圆括号的情况只有一种: 赋值语句的非模式部分, 可以使用圆括号。
js
[(b)] = [3]; // 正确
({ p : (d) } = {}); // 正确
[{parseInt.prop}] = [3]; // 正确
上面三行语句都可以正常执行,首先因为它们都是赋值语句,不是声明语句;其次它们的圆括号都不属于模式的一部分。第一行语句,模式是数组是取数组第一个成员,与圆括号无关;第二行模式是p
,而不是d
;第三行语句与第一行语句的性质一致。
参考 | 推荐
ECMAScript 6 入门 - 《阮一峰 ECMAScript 6 (ES6) 标准入门教程 第三版》 - 书栈网 · BookStack