Destructuring、Rest 與 Spread 運算


ES6 支援 Destructuring 的語法,它的概念像是模式比對(Pattern match)(然而不完全是),當你將某個結構拆解並分別指定給變數時,經常出現某種模式時,就可以使用這類語法。例如:

var scores = [80, 90, 99];
var score0 = scores[0];
var score1 = scores[1];
var score2 = scores[2];

scores 的結果,可能來自某個函式傳回值,像這樣的例子,在 ES6 中可以寫成:

let scores = [80, 90, 99];
let [score0, score1, score2] = scores;

在這個例子中使用陣列,該說它是陣列解構(Array destructing)嗎?實際上,只要是可迭代的物件,也就是具有可傳回迭代器的特性,都可以運用這種語法,例如字串:

> let [a, b, c] = 'XYZ';
undefined
> a;
'X'
> b;
'Y'
> c;
'Z'
>

變數的個數可以少於可迭代的元素數量,多餘的元素只是不被理會而已,又或者可以使用 Rest 運算:

> let lt = [10, 9, 8, 7, 6];
undefined
> let [head, ...tail] = lt;
undefined
> head;
10
> tail;
[ 9, 8, 7, 6 ]
>

Rest 運算 ... 會將剩餘的元素迭代出來指定給 tail,這樣的話,寫函數式的程式碼就方便多了,來玩一下:

function sum(numbers) {
    let [head, ...tail] = numbers;
    if(head) {
        return head + sum(tail);
    } else {
        return 0;
    }
}

console.log(sum([1, 2, 3, 4, 5])); // 15

如果可迭代的元素個數少於變數的數量,那麼多的變數會是 undefined,這是上面的函式得以運作的原因,事實上,參數也可以運用解構語法,只要這麼寫就好了(要再函數式風格的話):

function sum([head, ...tail]) {
    return head ? head + sum(tail) : 0;
}

console.log(sum([1, 2, 3, 4, 5])); // 15

如果可迭代的元素個數少於變數的數量,也可以指定變數的預設值,例如底下的 c 會是 3:

let [a, b, c = 3] = [1, 2];

如果只對某幾個元素有興趣呢?空下來就好了:

> let [x, ,y, , z] = [1, 2, 3, 4, 5];
undefined
> x;
1
> y;
3
> z;
5
>

我不贊成這麼寫就是了,如前面談到的,它的概念像是模式比對(Pattern match),當你將某個結構拆解並分別指定給變數時,經常出現某種模式時,就可以使用這類語法,因此,像上面的語法,就要檢討一下,你的程式中經常有這種模式嗎?不然只會在閱讀上造成困惑吧!

當然,也許像函數式之類風格時,就可以運用一下,例如就只是對尾元素感興趣:

> let [, ...tail] = [1, 2, 3, 4, 5];
undefined
> tail;
[ 2, 3, 4, 5 ]
>

由於有了解構語法,現在可以來玩玩 Python 風格的變數置換:

> let x = 10, y = 20;
undefined
> [x, y] = [y, x];
[ 20, 10 ]
> x;
20
> y;
10
>

在 ES6 中,若是指定的場合,... 可以用來當作 Rest 運算,若是放在某個可迭代物件之前,那它可以用來散佈(Spread)變數,例如:

> let arr = [1, 2, 3];
undefined
> let arr2 = [...arr, 4, 5];
undefined
> arr2;
[ 1, 2, 3, 4, 5 ]
> function plus(a, b) {
...     return a + b;
... }
undefined
> plus(...[1, 2]);
3
>

在 ES5 時,如果你的引數已經收集為陣列了,在〈this 是什麼?〉中談過,可以使用 Functionapply 方法,而在 ES6 中,如上看到的,直接使用 ... 就可以了。

類似地,物件也可以解構,在過去,如果你經常有以下的模式:

var o = {x : 10, y : 10};
var a = o.x;
var b = o.y;

在 ES6 中,可以寫成:

let o = {x : 10, y : 10};
let {x : a, y : b} = o;

唔!x 特性會指定給 a 變數,y 特性會指定給 b 變數,這跟 = 指定是相反的,一開始有點違反直覺,大概只是這麼記:「物件實字中 : 左邊一直都是特性」。

如果物件上沒有對應的特性呢?可以指定預設值:

let o = {x : 10, y : 10};
let {x : a, y : b, z : c = 20} = o;

上面特地讓變數與物件特性不同名稱,這是為了讓你知道誰指定給誰,因為如果變數與物件特性名稱相同的話,一開始你可能會搞不清楚誰指定給誰:

let o = {x : 10, y : 10};
let {x : x, y : y} = o;

像這種時候,可以簡單寫成:

let o = {x : 10, y : 10};
let {x, y} = o;

如果有預設值的話,可以如下:

let o = {x : 10, y : 10};
let {x, y, z = 10} = o;

沒有指定預設值的話,z 會是 undefined,要記得的是,第二行的 xy 是變數,不是 ox 特性,因此,o.x 被指定為 30 的話,x 變數是不受影響的。

物件解構語法也可以用在函式的參數上:

> function foo({x, y}) {
...     console.log(x);
...     console.log(y);
... }
undefined
>
> foo({x : 10, y : 10});
10
10
undefined
>

無論是方才的迭代器解構,或者是物件解構,都可以形成巢狀結構,例如:

let [[x, y, z], b, c] = [[1, 2, 3], 4, 5];

或者是:

let {a: {x, y, z}, b, c} = {a: {x: 10, y: 20, z: 30}, b: 40, c: 50};

再加上預設值、Rest 等語法,可以把它寫得很複雜,這你在其他 ES6 的文件中應該有看過,只是我看得頭都痛了,要不要寫成那樣呢?先問問自己在解構變數時,是否真的一而再、再而三的出現某個模式吧!