JS 實戰 (2) —— 從 Object.freeze 到 Immer.js:不可變性防禦
在上一篇中,我們學習了如何利用 get / set 讓物件變得「好動」(響應式)。但有趣的是,在 React 系列或是現代函數式編程中,大家追求的是相反的事:讓物件「動彈不得」(不可變性)。
為什麼我們需要不可變性?又該如何在高效率與安全性之間取得平衡?
一、 不可變性的起源:為什麼要這麼麻煩?
如果一個物件隨時都能被任何人修改,程式會變得極難預測:
- 副作用 (Side Effects):你傳一個物件進去函式,回來後它突然變了,你完全不知道是誰改的。
- 比較開銷:要判斷物件是否改變,你必須「深層比對」所有屬性,這效能極差。
不可變性的核心邏輯:如果你改了資料,你就給我一個「全新」的物件。 這樣我只需要比對記憶體位址(old === new),就能瞬間知道資料變了。
二、 原生防禦:const 的大誤區
很多初學者以為用 const 就能防禦,這是錯的。
javascript
const user = { name: "Linyi" };
user.name = "Xiu"; // ✅ 成功修改,const 只保護變數位址,不保護物件內容1. 初級防線:Object.freeze()
Object.freeze 能讓物件進入「唯讀模式」。
javascript
const config = Object.freeze({ theme: "dark" });
config.theme = "light"; // ❌ 靜默失敗或報錯(嚴格模式)> **地雷:淺層凍結 (Shallow Freeze)** > `Object.freeze` 只會凍結第一層。如果屬性是另一個物件,對面還是可以被修改。
三、 深拷貝的效能惡夢
為了實現不可變,最暴力的方法是「深拷貝」:
javascript
const newState = JSON.parse(JSON.stringify(oldState));
newState.user.age = 20;缺點:
- 效能極差:如果物件很大(幾萬筆資料),每次路過都要複製一遍,瀏覽器會當掉。
- 遺失型別:會弄丟 Date、Map、Set 以及函式。
四、 救星降臨:Immer.js 的 Proxy 魔法
Immer.js 是目前業界處理不可變數據最頂尖的工具,它的核心技術正是我們上一篇提到的 Proxy。
1. 寫起來是可變,跑起來是不可變
Immer 提供了一個 produce 函式,讓你用一般的寫法去改資料,但它最後會回傳一個全新的物件。
javascript
import { produce } from "immer";
const baseState = {
todo: [{ title: "Learn JS", done: false }],
other: "don't touch me",
};
const nextState = produce(baseState, (draft) => {
draft.todo[0].done = true; // ✅ 就像在改普通物件一樣
});
console.log(baseState.todo[0].done); // false (原始資料沒變)
console.log(nextState.todo[0].done); // true (新資料變了)
console.log(baseState.other === nextState.other); // true (沒變的部分共享記憶體)2. 底層原理:結構共享 (Structural Sharing)
Immer 的 Proxy 攔截了你所有的 set 操作。
- 它不會複製整個物件。
- 它只會複製「你修改過的那條路徑」。
- 沒修改過的分支,新物件會直接指向舊物件的位址(連位址都一樣)。
這就是所謂的 結構共享——既安全又省記憶體。
五、 實戰對比:React 裡的應用
如果你在 React 裡要更新一個深層物件:
❌ 原生 JS (痛苦且易出錯)
javascript
setUsers((prev) => ({
...prev,
info: {
...prev.info,
address: { ...prev.info.address, city: "Taipei" },
},
}));✅ 使用 Immer (簡潔直覺)
javascript
setUsers(
produce((draft) => {
draft.info.address.city = "Taipei";
})
);總結
不可變性不是為了限制你,而是為了讓你的程式碼更易於追蹤與維護。
- 小型場景:使用
const+...展開運算符即可。 - 唯讀配置:使用
Object.freeze(記得要深層遞歸處理)。 - 複雜狀態管理:強烈建議引入 Immer.js,讓 Proxy 幫你處理那些瑣碎的結構共享。
️ 進階挑戰
- 實作挑戰:試著寫一個
deepFreeze函式,它會遞歸遍歷物件並執行Object.freeze。 - 深度思考:在 Vue 中我們擁抱 Proxy 實現的可變性,在 React 中我們擁抱 Immer 實現的不可變性,這兩者的設計哲學有什麼本質區別?哪一個更有利於開發大型應用?
延伸閱讀與資源
- Immer.js 官網: immerjs.github.io
- MDN: Object.freeze()
- Immutable.js: 另一種基於 Trie 樹的不可變實作