若今天有一個要送人的水果藍,裡面依序放了西瓜、葡萄、酪梨。
const basket = ['watermelon', 'grapes', 'avocado']
結果客人甲告知對葡萄過敏所以要改成香蕉如下
// mutable
const basket = ['watermelon', 'grapes', 'avocado']
basket[1] = 'banana'; // 去動到本來物件內東西也是 mutable
basket // ['watermelon', 'banana', 'avocado']
葡萄從此徹底消失在這果菜市場裡因為再也不進貨。結果客人乙發火,他買這邊的水果箱就是為了吃葡萄,結果現在沒葡萄了氣的要退貨!怎麼辦呢 ?
以上就是常在 JS 發生的故事,因為 JS 原生資料結構都是 mutable 的。
可變動的,也就是定義好的 data 會隨著不同情況做變動。mutable 很容易會有無法預期的 bug 產生。
Immutable is whose state cannot be modified after it is created.
FP 是 immutable data ,也就是被定義好的 data 不會在任何情況下被改變,所以你能夠清楚預期他的內容。反之 mutable 是會變動的。
但你還是用 JS 啊,要怎麼做才能克服先天障礙 ? 最簡單方式就是複製原本 data 出來再修改
// immutable
const basket = ['watermelon', 'grapes', 'avocado']
const newBasket = [...basket] // 複製一份
newBasket[1] = 'banana';
basket // ['watermelon', 'grapes', 'avocado'] 跟原本一樣
newBasket // ['watermelon', 'banana', 'avocado']
這樣就算需要對原本 Basket 做別的事情也是可以的,以記憶體角度解釋,immutable 作法就是每當有一個新的操作,就會有新的區塊紀錄資料,指標也會指向新區塊。
而 mutable 則是從頭到尾都是同一個區塊(不會有新區塊產生),只是內容變成新資料而已 ,這也是 JS 中 by reference (傳址) 的概念。
但這樣就完美了嗎?
其實有一個很大的缺點,因為他必須複製一整份 Array,如上圖一樣,西瓜跟酪梨位置完全沒動也需要被複製,若現在 Array.length = 1000 的話這樣做非常浪費記憶體空間要會導致效能差
感謝許多工程師也發現這個問題所以才有 "Immutable data structure" 出現可以大大改善這個問題,之後也會寫一篇文章補充介紹。
網路上最推的方法不外乎是運用 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 我這邊就不再說了~
如有錯誤或需要改進的地方,拜託跟我說。
我會以最快速度修改,感謝您
歡迎追蹤我的部落格,除了技術文也會分享一些在矽谷工作的甘苦。
這個範例:
// 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);
對 是我寫錯了,先改成比較笨但比較好理解的
const newBasket = [...basket.slice(0,1), 'banana', ...basket.slice(2)];
thanks