iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 6
1
Software Development

Functional Programming in JS系列 第 6

Buzz Word 3 : Immutable vs. Mutable Data

若今天有一個要送人的水果藍,裡面依序放了西瓜、葡萄、酪梨。
https://ithelp.ithome.com.tw/upload/images/20200906/20106426BcvU1GeANi.jpg

const basket = ['watermelon', 'grapes', 'avocado']

結果客人甲告知對葡萄過敏所以要改成香蕉如下
https://ithelp.ithome.com.tw/upload/images/20200906/201064260cV1Glr8LV.jpg

// mutable
const basket = ['watermelon', 'grapes', 'avocado']
basket[1] = 'banana'; // 去動到本來物件內東西也是 mutable
basket // ['watermelon', 'banana', 'avocado']

葡萄從此徹底消失在這果菜市場裡因為再也不進貨。結果客人乙發火,他買這邊的水果箱就是為了吃葡萄,結果現在沒葡萄了氣的要退貨!怎麼辦呢 ?

以上就是常在 JS 發生的故事,因為 JS 原生資料結構都是 mutable 的。

Mutable data

可變動的,也就是定義好的 data 會隨著不同情況做變動。mutable 很容易會有無法預期的 bug 產生。

Immutable data

Immutable is whose state cannot be modified after it is created.

FP 是 immutable data ,也就是被定義好的 data 不會在任何情況下被改變,所以你能夠清楚預期他的內容。反之 mutable 是會變動的。

但你還是用 JS 啊,要怎麼做才能克服先天障礙 ? 最簡單方式就是複製原本 data 出來再修改
https://ithelp.ithome.com.tw/upload/images/20200906/20106426c7PHzjt5j0.jpg

// immutable
const basket = ['watermelon', 'grapes', 'avocado']
const newBasket = [...basket] // 複製一份
newBasket[1] = 'banana'; 
basket // ['watermelon', 'grapes', 'avocado'] 跟原本一樣
newBasket // ['watermelon', 'banana', 'avocado'] 

這樣就算需要對原本 Basket 做別的事情也是可以的,以記憶體角度解釋,immutable 作法就是每當有一個新的操作,就會有新的區塊紀錄資料,指標也會指向新區塊。

https://ithelp.ithome.com.tw/upload/images/20200906/20106426kAUOyflKLt.jpg

而 mutable 則是從頭到尾都是同一個區塊(不會有新區塊產生),只是內容變成新資料而已 ,這也是 JS 中 by reference (傳址) 的概念。

https://ithelp.ithome.com.tw/upload/images/20200906/20106426e6coAQeqCW.jpg

但這樣就完美了嗎?

其實有一個很大的缺點,因為他必須複製一整份 Array,如上圖一樣,西瓜跟酪梨位置完全沒動也需要被複製,若現在 Array.length = 1000 的話這樣做非常浪費記憶體空間要會導致效能差

https://ithelp.ithome.com.tw/upload/images/20200906/20106426GpUR8D8FQj.jpg

感謝許多工程師也發現這個問題所以才有 "Immutable data structure" 出現可以大大改善這個問題,之後也會寫一篇文章補充介紹。

確保團隊不會使用 mutable 的方式操作資料

網路上最推的方法不外乎是運用 immutable.js

// Vanilla JS
const basket = ['watermelon', 'grapes', 'avocado']
const newBasket = [...basket.slice(0,1), 'banana', ...basket.slice(2)];
newBasket // ['watermelon', 'banana', 'avocado'] 


// immutable
const basket = ['watermelon', 'grapes', 'avocado']
const newBasket = replaced(basket, 1, 'banana');
console.log(newBasket);

// Immutable.js
const { List } = require('immutable');
const basket = List(['watermelon', 'grapes', 'avocado']);
const newBasket = basket.set(1, 'banana');
// 不會影響原本陣列
console.log(newBasket.toJS()) // ["watermelon", "banana", "avocado"]
console.log(basket.toJS()) // ["watermelon", "grapes", "avocado"]

newBasket 回傳一個新的 Array,對原本 basket 完全不會受到影響,這就是 immutable ~

Redux Toolkit 也內建 immutable library 讓你可以看似 mutable 寫法但 compile 出來是 immutable 的 (畢竟大家還是對 mutable 寫法比較習慣)

但如果不想額外用 library 只想用 Vanilla JS 做到 immutable,也可以用 我們的 Function 不一樣 推薦的 eslint-plugin-immutable 確保團隊都有用 immutable 方法操作資料,文章裡面也有建議的設定 rule 我這邊就不再說了~


參考文章

如有錯誤或需要改進的地方,拜託跟我說。
我會以最快速度修改,感謝您

上一篇
Buzz word 2 : Side Effect
下一篇
把 Mutable array/object 轉成 Immutable
系列文
Functional Programming in JS30

1 則留言

2
良葛格
iT邦新手 3 級 ‧ 2020-09-06 11:42:40

這個範例:

// immutable
const basket = ['watermelon', 'grapes', 'avocado']
const newBasket = [...basket] // 複製一份
newBasket[1] = 'banana'; 
basket // ['watermelon', 'grapes', 'avocado'] 跟原本一樣
newBasket // ['watermelon', 'banana', 'avocado'] 

newBasket[1] = 'banana' 就是做了 mutable 的動作了,如果整個流程都想 immutable,可以這麼寫:

function replaced(src, i, item) {
    return src.slice(0, i).concat([item], src.slice(i + 1));
}

// immutable
const basket = ['watermelon', 'grapes', 'avocado']
const newBasket = replaced(basket, 1, 'banana');
console.log(newBasket);
hannahpun iT邦新手 5 級 ‧ 2020-09-06 14:04:08 檢舉

對 是我寫錯了,先改成比較笨但比較好理解的

const newBasket = [...basket.slice(0,1), 'banana', ...basket.slice(2)];

thanks

我要留言

立即登入留言