Destructuring、Rest 與 Spread
August 7, 2022ES6 以後支援簡化的模式比對(Pattern matching) 語法,像是 Destructuring、Rest 與 Spread,基於資料的模式來拆解、分配資料,相對於基於索引、切片之類的方式,善加利用這類簡化的模式比對,有助於程式碼的可讀性
Destructuring/Rest
當你將某個結構拆解並分別指定給變數時,經常出現某種模式時,就可以使用這類語法。例如:
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'
>
變數的個數可以少於可迭代的元素數量,多餘的元素只是不被理會而已。
接著來看看,相較於使用以下的方式:
> var lt = [10, 9, 8, 7, 6];
undefined
> var head = lt[0];
undefined
> var tail = lt.slice(1);
undefined
> head
10
> tail
[ 9, 8, 7, 6 ]
>
不如使用 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
事實上,參數也可以運用 Destructuring/Rest 語法,只要這麼寫就好了(要再函數式風格的話):
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
>
Spread
在 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 是什麼?〉談過,可以使用 Function
的 apply
方法,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
,要記得的是,第二行的 x
、y
是變數,不是 o
的 x
特性,因此,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 的文件中應該有看過,只是我看得頭都痛了,要不要寫成那樣呢?先問問自己在解構變數時,是否真的一而再、再而三的出現某個模式吧!