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");