数组解构赋值
用法
ES6允许按照一定模式,从数组和对象中提取值,对变量进行赋值,称为解构(destructuring)。
用法:
var [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 [x, , y] = [1, 2, 3];
x // 1
y // 3
let [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4]
var [x, y, ...z] = ["a"];
x // "a"
y // undefined
z // []
如果解构不成功,变量的值就为undefined,即故障弱化(fail-soft destructuring)。
var [foo] = [];
foo // undefined
var [bar, foo] = [1];
bar // 1
foo // undefined
以上两种情况都属于解构不成功,foo的值都为undefined。
另外还存在一种不完全解构模式,即等号左边的模式只匹配等号右边一部分数组。这种情况下,解构仍然可以成功。如下:
let [x, y] = [1, 2, 3];
x // 1
y // 2
let [a, [b], c] = [1, [2, 3], 4];
a // 1
b // 2
c // 4
如果等号右边不是数组(或者严格说,不是可遍历的结构,参见Iterator),将会报错。如下:
var [foo] = 1;
var [foo] = false;
var [foo] = NaN;
var [foo] = undefined;
var [foo] = null;
var [foo] = {};
注意,解构赋值不仅适用于var,也适用于let和const命令,如上面的例子中也使用了let命令。
var [a, b] = [1, 2];
let [a, b] = [1, 2];
const [c, d] = [3, 4];
对于Set结构,也可以使用数组的解构赋值。如下:
let [x, y, z] = new Set(['a', 'b', 'c']);
x // "a"
y // "b"
z // "c"
默认值
解构赋值允许指定默认值。如下:
var [foo = true] = [];
foo // true
var [x, y = 'b'] = ['a'];
x // "a"
y // "b"
注意,ES6内部使用严格等于运算符,即’===’,判断一个位置是否有值。所以,如果一个数组成员不严格等undefined,默认值是不会生效的。
var [x = 1] = [undefined];
x // 1
var [x = 1] = [null];
x // null
如果默认值是一个表达式,则这个表达式是惰性求值的,即只有在用到的时候才会求值。
function f() {
return 2;
}
var [x = f()] = [undefined];
x // 2
默认值也可以引用解构赋值的其它变量,但该变量必须已经声明。如下:
var [x = 1, y = x] = []; // x = 1; y = 1
var [x = 1, y = x] = [2]; // x = 2; y = 2
var [x = 1, y = x] = [1, 2]; // x = 1; y = 2
var [x = y, y = 1] = []; // ReferenceError
上面最后一个表达式会报错,是因为x用到默认值y,此时y还没有声明。
对象解构赋值
解构同样还可以用于对象。如下:
var {foo, bar} = {foo: "aaa", bar: "bbb"};
foo // "aaa"
bar // "bbb"
对象的解构与数组的一个重要不同是,数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名才能取到正确的值。
var {bar, foo} = {foo: "aaa", bar: "bbb"};
foo // "aaa"
bar // "bbb"
var {baz} = {foo: "aaa", bar: "bbb"};
baz // undefined
上面例子中,第一个等号左边的两个变量的次序与右边的两个同名属性的次序不一致,但是对取值没有任何影响;第二个等式的变量没有与对应的同名属性,导致变量取不到值,为undefined。
如果变量名与对象属性名不一致,必须写成如下形式:
var {foo: baz} = {foo: "aaa", bar: "bbb"};
baz // "aaa"
var obj = {first: 'hello', last: 'world'};
var {first: f, last: l} = obj;
f // "hello"
l // "world"
实际上,这说明对象的解构赋值上下面形式的简写:
var {foo: foo, bar: bar} = {foo: 'aaa', bar: 'bbb'};
也就是说,对象的解构赋值的内部机制是先找到同名属性,然后再赋值给对应的变量,真正被赋值的是后者,而不是前者。
var {foo: baz} = {foo: "aaa", bar: "bbb"};
baz // aaa
foo // ReferenceError: foo is not defined
上面代码中,真正被赋值的是变量baz,而不是模式foo。
注意,采用这种写法时,变量的声明和赋值是一体的,对于let和const来说,变量不能重新声明,一旦赋值的变量以前声明过,就会报错。
let foo;
let {foo} = {foo: 1}; // TypeError: identifier foo has already been declared
let baz;
let {bar: baz} = {bar: 1}; // TypeError: identifier baz has already been declared
上面例子中,解构赋值的变量都会重新声明,所以报错。但var命令允许重复声明,所以这个错误只会在使用let和const命令时出现。
和数组一样,对象解构也可以用于嵌套结构的对象。如下:
var obj = {
p: [
'Hello',
{y: 'world'}
]
};
var {p: [x, {y}]} = obj;
x // "Hello"
y // "world"
注意,此时p是模式,不是变量,因而不会被赋值。同样如下:
var node = {
loc: {
start: {
line: 1,
column: 5
}
}
};
var {loc: {start: {line, column}}} = node;
loc // ReferenceError: loc is not defined
start // ReferenceError: start is not defined
line // 1
colume // 5
上面代码中,只有line和column是变量,而loc和start都是模式,所以不会被赋值。
对象的解构也可以指定默认值,如下:
var {x = 3} = {};
x // 3
var {x, y = 1} = {x: 3};
x // 3
y // 1
var {message: msg = 'Something went wrong.'} = {};
msg // "Something went wrong."
message // ReferenceError: message is not defined
默认值生效的条件是,对象的同名属性值严格等于undefined。
var {x = 3} = {x: undefined};
x // 3
var {x = 3} = {x: null};
x // null
上面第二个等式中,x的同名属性值为null,不严格等于undefined,所以默认值不会生效。
如果解构失败,变量的值就等于undefined。
var {foo} = {bar: 'bar'};
foo // undefined
解构赋值允许等号左边的模式之中,不放置任何变量名。因此,可以写出非常古怪的赋值表达式,如下例子,这些表达式都毫无意义,但语法是正确的,可以执行。
var {} = [true, false];
var {} = "abc";
var {} = [];
对象的解构赋值可以很方便地将现有对象的方法,赋值给某个变量,如下:
let {log, sin, cos} = Math;
上面代码可以将Math对象的对数、正弦和余弦三个方法赋值到对应的变量上,方便使用。
字符串解构赋值
字符串也可以解构赋值,因为此时字符串被转换成了一个类似数组的对象。如下,
let {a, b, c, d, e} = "hello";
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"
类数组对象都有一个长度属性length,因此还可以对这个属性解构赋值。
let {length: len} = 'hello';
len // 5
函数参数解构赋值
函数参数也可以使用解构赋值。如下,
function add([x, y]) {
return x + y;
}
add([1, 2]); // 3
上面代码中,函数add的参数表面上是一个数组,但在传入参数的那一刻,数组参数被解构成变量x和y。对于函数内部来说,它们能感受到的参数就是x和y。
下面是另一个例子,
[[1, 2], [3, 4]].map(([a, b]) => a + b);
// [3, 7]
函数参数的解构出可以使用默认值,
function move({x = 0, y = 0} = {}) {
return [x, y];
}
move({x: 3, y:4}); // [3, 4]
move({x: 3}); // [3, 0]
move({}); // [0, 0]
move(); // [0, 0]
上面代码中,函数move的参数是一个对象,通过对这个对象进行解构,得到变量x和y的值。如果解构失败,则x和y等于默认值。
注意,下面的写法会得到不同的结果,
function move({x, y} = {x: 0, y: 0}) {
return [x, y];
}
move({x: 3, y:4}); // [3, 4]
move({x: 3}); // [3, undefined]
move({}); // [undefined, undefined]
move(); // [0, 0]
上面代码是为函数move的参数指定默认值,而不是为变量x和y指定默认值,所以结果会不同。
用途
变量的解构赋值用途很多。下面列出几个方面:
(1) 交换变量的值
[x, y] = [y, x];
上面代码交换变量x和y的值,简洁易读,且语义非常清晰。
(2) 从函数返回多个值
函数只能返回一个值,如果要返回多个值,只能将它们放在数组或对象里返回,有了解构赋值,取出这些值就非常方便。
// 返回一个数组
function func() {
return [1, 2, 3];
}
// 返回一个对象
function func() {
return {
a: 2,
b: 3
};
}
var {a, b} = func();
a // 2
b // 3
(3) 函数参数的定义
解构赋值可以方便地将一组参数与变量名对应起来,快速提取JSON对象中的值。
// 参数是数组
function func([x, y, z]) { ... }
func([2, 3, 4]);
// 参数是对象
function func({x, y, z}) { ... }
func({z: 3, x: 4, y: 5});
(4) 提取JSON数据
解构赋值对于提取JSON对象中的数据非常有用。
var jsonData = {
id: 2,
status: "OK",
data: [123, 456]
};
let {id, status, data: number} = jsonData;
console.log(id, status, number);
// 2 "OK" [123, 456]
(5) 函数参数的默认值
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’;这样的语句。
(6) 遍历Map结构
任何部署了Iterator接口的对象,都可以用for…of循环遍历。Map结构原生支持Iterator接口,配合变量的解构赋值,获取键名和键值就非常方便。
var map = new Map();
map.set('first', 'hello');
map.set('second', 'world');
for (let [key, value] of map) {
console.log(key + " is " + value);
}
// first is hello
// second is world
如果只想获取键名,或者只相获取键值,可以写成如下:
// 获取键名
for (let [key] of map) {
console.log(key);
}
// 获取键值
for (let [, value] of map) {
console.log(value);
}
(7) 输入模块的指定方法
加载模块时,往往需要指定输入的那些方法,解构赋值就使得输入语句非常清晰。
const {SourceMapConsumer, SourceNode} = require("source-map");