iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 4
1

本章重點

  • 如何使用 Spread Operator 複製 Reference type variable 。
  • 大推 Immutable.js ,輕鬆上手、簡單好用。

在開始之前

上章說明了使用 Immutable Data 是如何,讓我們更容易的去處理緩存、Undo、簡化開發複雜度。
建議在開始之前可以先讀 上一篇:這個 Object 怪怪的

禁止改寫變數,只能複製它

禁止改寫變數,這也意味著不能使用 Array.pushArray.splice ,我們得使用其他方法複製 Array ,在 JS 可以使用 Array.concat 複製 Array ,在 ES6 可以 Spread Operator (希望你還記得JavaScript Syntax 大集合)。

const array1 = [1, 2, 3, 4]

// ES5
const array2 = [].concat(array1)

// ES6
const array3 = [...array1]

console.log(
    array1 === array2, // false
    array1 === array3 // false
)

Spread Operator 也可以輕鬆達到 push 、 unshift 、 remove 。

const push = (x, array) => [...array, x]
const unshift = (x, array) => [x, ...array]
const remove = (index, array) => [...x.slice(0, index), ...x.slice(index + 1)]

Object 則是使用 Object.assign,但 ES6 還是可以用 Spread Operator 。

const object1 = {
	name: "wl00887404"
}
// ES5
const object2 = Object.assign({}, object1)

// ES6
const object3 = {
	...object1
}

console.log(
    object1 === object2, // false
    object1 === object3 // false
)

如此複製不會造成效能上的問題嗎?
當然這些操作是有成本的,但這些操作並非 deep copy ,而是 shallow copy ,效能影響跟帶來的好處相比是小菜一碟,來看看這段程式碼。

const tree = {
	value: 5,
	left: {
		value: 4
	},
	right: {
		value: 7,
		left: {
			value: 6
		}
	}
}

const right = {
	...tree.right,
	value: 8,
	right: {
		value: 9
	}
}

const mTree = {
	...tree,
	right
}

console.log(
    mTree === tree, // false
    mTree.left === tree.left, // tree
    mTree.right === tree.right, // false
    mTree.right.left === tree.left, // false
    mTree.right.left === tree.right.left // true
)

複製的情況就如同這張圖:

其中 (綠色) 5 、 8 、 9 是新節點,而 (藍色) 4 、 6 則是舊節點,並沒有被複製,只是被新的節點連起來。
shallow copy 是淺拷貝,它並不像 deep copy 是複製所有節點,而是共享沒有變動的部分,如此可以有較好的效能與記憶體利用。

而深拷貝的做法,各位可以參考 Jqery 的 extends 或是 Lodash 的 cloneDeep ,因為比較少用,這裡就不再介紹了。

Const 不夠安全

為了達到 Immutable ,我們不能破壞已經賦值的變數,很自然的在 JS 中讓人想到 const ,本系列之前的 function 宣告也都是用 const,那讓我們親手試試看以下的程式碼。

const array = []
array = []

//  Uncaught TypeError: Assignment to constant variable.
//    at <anonymous>:1:7
const array = []
array.push(1)

console.log(
    array // [1]
)

const 並不是沒有起作用 , 我們依然無法重新賦值 array , 但卻可以藉由 push 改變它,因為 Object 在 JS 中預設是 Mutalbe ( Array 也是一種 Object )。
如此與理想中的 Immutable 不同,為了達到這個目的,必須使用 Object.freeze

const array = []
Object.freeze(array)
array.push(1)

// Uncaught TypeError: Cannot add property 1, object is not extensible
//   at Array.push (<anonymous>)

那我們來試試兩層的 Reference。

let obj = {
    name: "wl00887404"
}
const array = [obj]
Object.freeze(array)

array[0].name = "foo"
console.log(
    array[0], // {name: "foo"}
    Object.isFrozen(array[0]) // false
)

可是 Object.freeze 只會針對當前的 Reference,並不會 recursively freeze,為了解決這些麻煩,可以使用 deep-freeze ,或是我推薦使用有 Immutable Data api 的 Immutable.js ,如果想從根本上解決,那就改成使用 ElmPureScript 吧。

Immutable.js

Immutable.js 為 FB 的開源專案,它提供了 7 種常用的資料結構 (List, Stack, Map, OrderedMap, Set, OrderedSet, Record) ,而且 api 是 FP style 再加上類似 mutable,讓它相當好上手。

// 將 map1.b 改為 87
const { Map } = Immutable
const map1 = Map({ a: 1, b: 2, c: 3 })
const map2 = map1.set("b", 87)

console.log(
    map1.get("b"), // 2
    map2.get("b"), // 87
    map2.toJS() // { a: 1, b: 87, c: 3 }
)
// 過濾掉奇數
// 加 1
// 並加總
const { List } = Immutable
const list1 = List([1, 2, 3, 4, 5, 6])
const list2 = list1
    .filter(x => x % 2 == 1)
    .map(x => x + 1)
const sumList2 = list2.reduce((acc, x) => acc + x)

console.log(
    list2.toJS(), // [2, 4, 6]
    sumList2 // 12
)

你可以在 這裡 嘗試看看,這是個有加入 Immutable.js 的 JS bin。

另外在效能方面, Immutable.js 大大優於 ES6 ,如果想知道詳情可以參考 Immutable.js, persistent data structures and structural sharing ,這篇非常詳細,也推薦各位可以看 React.js Conf 2015 - Immutable Data and React,很明確的解說 Immutable.js 是如何實作的.

關於是否一定要使用 Immutable.js ,我的結論與上述的文章相同,如果相當需要優化效能,用 Immutable.js 準沒錯,譬如說我曾經要處理約 70 萬筆的空氣資料,那時我就選擇用 Immutable.js ,如果資料不大,這就是個人選擇了。

後記

今天是 Immutable Data 實作,之後的程式碼將會大量看到 Spread Operator , Immutable.js 如果真要說有什麼缺點,那大概就是要記得 toJS() 吧,我很常忘記 XD ,另外印象中我有個朋友說要用 python 把這個系列的程式全都改寫一遍,我們期待他。
明天來介紹 Pure Function ,如果懂得 Immutable Data 應該是輕輕鬆鬆。

另外誠徵幽默小寫手,截圖取自我的省文小幫手

參考資料


上一篇
這個 Object 好像怪怪的
下一篇
Pure Function 很純的函數
系列文
30天快樂學習 Functional Programming14

1 則留言

0
hmch17
iT邦新手 5 級 ‧ 2017-12-25 04:42:46

map2.get("b"), // 50
這註解的值應該是87

阿志 iT邦新手 5 級‧ 2017-12-25 06:03:39 檢舉

已修正,謝謝你!

我要留言

立即登入留言