點燈坊

失くすものさえない今が強くなるチャンスよ

ECMAScript 之 Spread Operator

Sam Xiao's Avatar 2020-07-08

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

Version

macOS Catalina 10.15.5
VS Code 1.46.1
ECMAScript 2015

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()

Merge 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 f = (x, y, z) => x + y + z

f(...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

String to Array

let str = 'Hello' // ?
let arr = [...str] // ?

Spread operator 亦支援 Iterable,而 String 就是最簡單的 Iterable,因此可將 String 轉成 Char Array。

spread008

Set to Array

let set = new Set([1, 2, 3]) // ?
let arr = [...set] // ?

Set 亦為 Iterable,因此 ... 亦可將 Set 轉成 Array。

spread009

Node List to Array

<template>
  <div id="app">
    <p>Hello</p>
    <p>World</p>
  </div>
</template>

<script>
let mounted = function() {
  let nodeList = document.querySelectorAll('p')
  let arr = [...nodeList]

  arr.forEach(x => console.log(x.innerText))
}

export default {
  name: 'App',
  mounted
}
</script>

第 10 行

let nodeList = document.querySelectorAll('p')
let arr = [...nodeList]

arr.forEach(x => console.log(x.innerText))

使用 document.querySelectorAll() 取得所有 HTML tag 為 <p> 的 DOM Element,其回傳為 Node List,並不是 Array。

可使用 ... 將 Node List 轉成 Array,因此才能使用 forEach()

spread010

Convert Arguments to Array

let f = function() {
  return [...arguments].map(x => `Hello ${x}`)
}

f('Sam', 'John') // ?

arguments 只是 Array-like object,可以使用 ... 轉成 Array 後,方可使用 map()

arguments 只能用在 function expression,不能在 arrow function

Conclusion

  • 關於 clone Array,ES5 可用 Array.prototype.slice(),ES6 也可用 Array.from()
  • 關於 merge 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
  • Spread operator 還可用在 Iterable,如 String、Set、Node List 與 Array-like Object

Reference

Adam Daniels, Top 5 Uses for the Spread Operator in JavaScript
Samantha Ming, 6 Use Case of Spread with Array in JavaScript
MDN, Spread syntax
MDN, Rest parameters