ES6 允许按照一定的模式从数组和对象中提取属性或值并将其赋值给其它变量,这就是 解构(Destructuring)赋值 。
// 完全解构
let [a, b, c] = [1, 2, 3]; // a:1, b:2, c:3
let [, , c] = [1, 2, 3]; // c:3
let [a, ...c] = [1, 2, 3]; // a:1, c:[2, 3]
let [a, b, c] = [1, 2]; // a:1, b:2, c:undefined
let [a, b, ...c] = ['a']; // a:'a', b:undefined, c:[]
// 不完全解构
let [a, b] = [1, 2, 3]; // a:1, b:2
let [a, [b], c] = [1, [2, 3], 4]; // a:1, b:2, c:4
上面的写法可以看做是一种 模式匹配 ,只要前后的模式一致,那么左边的变量就会被赋值成对应的值。
我们通常认为,Set
集合也是一种特殊的数组结构,所以,Set
也不例外的可以使用数组的解构赋值:
let [x, y, z] = new Set([1, 2, 3]); // x:1, y:2, z:3
实质上,只要某种数据结构具有 Iterator 接口就可以使用数组的解构赋值:
// (http://es6.ruanyifeng.com/#docs/destructuring)
// https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/function*
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
为了防止从数组中取值得到是 undefined
,可以对表达式左侧的任意属性给予默认值。
let [a=1] = []; // a:1
let [a=1] = [undefined]; // a:1
let [a=1] = [null]; // a:null
let [x = 1, y = x] = [1, 2]; // x=1; y=2
let [x = y, y = 1] = []; // ReferenceError: y is not defined
ES6 内部使用严格意义上的 ===
进行比较,所以我们并不能避免得到的值为 null
。
如果默认值是一个表达式,那么这个表达式需要是 惰性求值 的,只有在用到的时候才求值:
function f() {
console.log('aaa');
}
let [x = f()] = [undefined];
// 'aaa' 会被立即输出,而不需要调用 x();
// 如果将 x = f() 改为 x = f 则需要手动调用 x();
对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。
let { bar, foo } = { foo: 'aaa', bar: 'bbb' };
foo // "aaa"
bar // "bbb"
let { baz } = { foo: 'aaa', bar: 'bbb' };
baz // undefined
对象的解构赋值,可以很方便地将现有对象的方法,赋值到某个变量:
const { log } = console;
log('Goodbye~ The world!'); // 'Goodbye~ The world!'
const { PI } = Math;
PI; // 3.141592653589793
如果你想给变量一个不同于对象属性的名称,可以使用下面这种方式:
const { log: echo } = console;
echo('Goodbye~ The world!'); // Goodbye~ The world!
log('Goodbye~ The world!'); // ReferenceError: log is not defined
// { foo, bar } = { foo: 'aaa', bar: 'bbb' } 的本质
// 这归功于 ES6 对 对象的扩展
let { foo: foo, bar: bar } = { foo: 'aaa', bar: 'bbb' };
下面看一个稍微复杂点的例子:
const node = {
loc: {
start: {
line: 1,
column: 5
}
}
};
let { loc, loc: { start }, loc: { start: { line }} } = node;
line // 1
loc // Object {start: Object}
start // Object {line: 1, column: 5}
上面的代码中执行了多次解构,这样写的好处是我们能够很清晰的找到对象的层级解构。注意,这种写法中只有最内层 {}
包裹的能称之为变量,如 loc: { start: { line } }
中只有 line
是变量(严格意义上来说是: line: line
后面的 line
为变量),其它都为模式,即不能直接使用 start
。
对象的解构赋值也可以基于默认值。
var {x = 3} = {};
x // 3
var {x, y = 5} = {x: 1};
x // 1
y // 5
var {x: y = 3} = {};
y // 3
var {x: y = 3} = {x: 5};
y // 5
var { message: msg = 'Something went wrong' } = {};
msg // "Something went wrong"
var {x = 3} = {x: undefined};
x // 3
var {x = 3} = {x: null};
x // null
将 {}
写在行首并不构成表达式,而是一个代码块。
// 错误的写法
let x;
{x} = {x: 1};
// SyntaxError: syntax error
// 正确的写法
let x;
({x} = {x: 1});
数组本质是特殊的对象,因此可以对数组进行对象属性的解构。
let arr = [1, 2, 3];
let {0 : first, [arr.length - 1] : last} = arr;
first // 1
last // 3
字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象。
const [h, e, l, l2, o] = 'hello';
h // "h"
e // "e"
l // "l"
l2 // 'l' // 变量名不能同为 'l', 否则 SyntaxError: Identifier 'l' has already been declared
o // "o"
类似数组的对象都有一个length属性,因此还可以对这个属性解构赋值。
let {length : len} = 'hello';
len // 5
前面说解构赋值用于从数组和对象中提取属性或值并将其赋值给其它变量,如果解构表达式的右值为数字或布尔类型的时候内部会先将其转换为对象:
let { toString } = 9;
toString === Number.prototype.toString; // true
let { toString } = true;
toString === Boolean.protype.toString; // true
由于 undefined
和 null
不能被转换为对象(虽然 null 通常被视作一个对象,但是它是空对象,不具有任何属性),所以对它们进行解构赋值会报错:
// TypeError: Cannot destructure property `prop` of 'undefined' or 'null'.
let { toString } = undefined;
// TypeError: Cannot destructure property `prop` of 'undefined' or 'null'.
let { toString } = null;
函数的实参和形参之间也可以进行模式匹配:
function add([x, y]) {
return x + y;
}
add([2, 3]); // 5
[[1,2], [3,4]].map(([a, b]) => a + b); // [3, 7]
函数参数的解构也可以有默认值:
function moveTo({x = 0, y = 0} = {}) {
return {x, y};
}
moveTo({ x: 10, y: 2 });// {x: 10, y: 2}
moveTo({ y: 3 }); // {x: 0, y: 3}
moveTo({ }); // {x: 0, y: 0}
moveTo(); // {x: 0, y: 0}
但是如果你按照下面的写法给函数参数的解构基于默认值就会出现问题:
function move({x, y} = { x: 3, y: 2 }) {
return [x, y];
}
move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, undefined]
move({}); // [undefined, undefined]
move(); // [3, 2]
实际上这个问题很好理解:如果函数的实参不为空,则会替换函数形参的右值。那么这里的 move({x: 3})
类似于 function move({x, y} = {x: 3})
,这下读出 {x, y} = {x: 3})
的结果为 x: 3, y: undefined
就很容易了。
当解构赋值语句中包含小括号 ()
的时候是很容易出错的:
// 全部报错
let [(a)] = [1];
let {x: (c)} = {};
let ({x: c}) = {};
let {(x: c)} = {};
let {(x): c} = {};
let { o: ({ p: p }) } = { o: { p: 2 } };
// 报错
function f([(z)]) { return z; }
// 报错
function f([z,(x)]) { return x; }
// 全部报错
({ p: a }) = { p: 42 };
([a]) = [5];
// 报错
[({ p: a }), { x: c }] = [{}, {}];
只有圆括号所包含的内容不是模式的一部分且非声明式语句的时候才允许被使用:
[(b)] = [3]; // 正确
({ p: (d) } = {}); // 正确
[(parseInt.prop)] = [3]; // 正确
let x = 1;
let y = 2;
[x, y] = [y, x];
function fn() {
return [1, 2, 3];
}
let [a, b, c] = fn();
let user = { name: 'ultravires', age: 12, email: '[email protected]' };
let {name, email} = user;
这在处理 JSON 数据的时候尤其有用,这也是解构赋值最方便的应用,它是大多数解构赋值应用的根本。
let nodes = document.getElementsByTagName('div');
[...nodes].forEach(item => { console.log(item); });
const { SourceMapConsumer, SourceNode } = require("source-map");