點燈坊

學而時習之,不亦悅乎

深入探討 Spread Operator

Sam Xiao's Avatar 2019-08-14

ECMAScript 2015 的 Spread Operator 是很有創意的發明,讓很多操作都簡化成 ... 即可,且可讀性更佳。

Version

macOS Mojave 10.14.5
VS Code 1.37.0
Quokka 1.0.240
ECMAScript 2015
Ramda 0.26.1

Clone Array

import { equals } from 'ramda';

let arr1 = [1, 2, 3];
let arr2 = [...arr1];

arr1 === arr2; // ?
equals(arr1, arr2); // ?

欲將 arr1 clone 到 arr2 ,注意是 clone 而非 copy,可在 [] 間使用 ...arr1 的 element 展開即可 clone。

spread000

使用 === 判斷為 false,可見 arr1arr2 的 reference 並不相同,因此並非 copy reference。

使用 Ramda 的 equals() 判斷為 true,可見 arr1arr2 的 value 相同,因此為 clone array。

若使用 assignment operator =,則為 copy reference,因此 ===equals() 皆回傳 true

import { clone } from 'ramda';

let arr1 = [
  1, 
  [3, 4],
];

let arr2 = [...arr1]; // ?
let arr3 = clone(arr1); // ?

arr1 === arr2; // ?
arr1 === arr3; // ?

arr1[1] === arr2[1]; // ?
arr1[1] === arr3[1]; // ?

不過 ... 只是 shallow clone,也就是第一層 array 為 clone,第二層之後的 array 為 copy reference。

arr1 內包含 nested array [3, 4],其中 arr2 使用 ...,而 arr3 使用 Ramda 的 clone()

spread006

無論使用 ...clone()arr2arr3 的 reference 皆與 arr1 不同。

  • arr2[1]arr1[1] 的 reference 相同,顯然 ... 是 shallow clone
  • arr3[1]arr1[1] 的 reference 不同,顯然 clone() deep clone

Spread operator 雖然 clone 很方便,但僅是 shallow clone,若要 deep clone 則要使用 Ramda 的 clone()

Concatenate Array

let arr1 = [1, 2, 3];
let arr2 = [4, 5, 6];

let arr3 = [...arr1, ...arr2]; // ?

欲將 arr1arr2 合併,可在 [] 內將 arr1arr2 的 element 使用 ... 展開,中間加上 , 即可合併。

Array 合併時亦有 ... shallow clone 問題需注意

spread001

Pass Arguments as Array

let data = [1, 2, 3];

let fn = (x, y, z) => x + y + z;

fn(...data); // ?

若原本 function 為 n 個 argument,但資料卻是 n 個 element 的 array,可使用 ... 將 array element 展開傳入 function。

spread002

Clone Object

import { equals } from 'ramda';

let obj1 = { name: 'Sam' };
let obj2 = { ...obj1 };

obj1 === obj2; // ?
equals(obj1, obj2); // ?

... 除了用於 array,亦可用於 object。

欲將 obj1 clone 到 obj2 ,注意是 clone 而非 copy,可在 {} 間使用 ...obj1 的 property 展開即可 clone。

spread003

使用 === 判斷為 false,可見 obj1obj2 的 reference 並不相同,因此並非 copy reference。

使用 Ramda 的 equals() 判斷為 true,可見 obj1obj2 的 property 相同,因此為 clone object。

若使用 assignment operator =,則為 copy reference,因此 ===equals() 皆回傳 true

import { clone } from 'ramda';

let obj1 = {
  id: 1,
  name: {
    first: 'Sam',
    last: 'Xiao'
  }
};

let obj2 = {...obj1}; // ?
let obj3 = clone(obj1); // ?

obj2 === obj1; // ?
obj3 === obj1; // ?

obj1.name === obj2.name; // ?
obj1.name === obj3.name; // ?

不過 ... 只是 shallow clone,也就是第一層 object 為 clone,第二層之後的 object 為 copy reference。

obj1.name 為 object,其中 obj2 使用 ...,而 obj3 使用 Ramda 的 clone()

spread006

無論使用 ...clone()obj2obj3 的 reference 皆與 obj1 不同。

  • obj2.nameobj1.name 的 reference 相同,顯然 ... 是 shallow clone
  • obj3.nameobj1.name 的 reference 不同,顯然 clone() deep clone

Spread operator 雖然 clone 很方便,但僅是 shallow clone,若要 deep clone 則要使用 Ramda 的 clone()

Merge Object

let obj1 = { firstName: 'Sam' };
let obj2 = { lastName: 'Xiao'};

let obj3 = {...obj1, ...obj2 }; // ?

欲將 obj1obj2 合併,可在 {} 內將 obj1obj2 的 property 使用 ... 展開,中間加上 , 即可合併。

Object 合併時亦有 ... shallow clone 問題需注意

spread004

Rest Parameter

let sum = (...args) => args.reduce((a, x) => a + x, 0);

sum(1, 2, 3); // ?

若要實現 function 有 無限 argument,可在 argument 名稱前加上 ...,此時 args 為真正 array,而非 array-like object,因此可使用 Array.prototype 下的 method。

使用 rest parameter 時,儘管 arrow function 的 argument 看起來只有一個,但也要加上 (),否則會誤判

spread005

Conclusion

  • 關於 clone array,ES5 可用 Array.prototype.slice(),ES6 也可用 Array.from()
  • 關於 concatenate array,ES5 可用 Array.prototype.concat()
  • 關於 pass argument as array,ES5 可用 Function.prototype.apply()
  • 關於 clone 與 merge object,ES5 可用 Object.prototype.assign()
  • 關於 rest parameter,ES5 可使用 function 自帶的 arguments variable,注意其為 array-like object 與 iterable,而非真正 array

Reference

Adam Daniels, Top 5 Uses for the Spread Operator in JavaScript
MDN, Spread syntax
MDN, Rest parameters