跳至主要內容
Skip to content

JS 實戰 (2) —— 從 Object.freeze 到 Immer.js:不可變性防禦

在上一篇中,我們學習了如何利用 get / set 讓物件變得「好動」(響應式)。但有趣的是,在 React 系列或是現代函數式編程中,大家追求的是相反的事:讓物件「動彈不得」(不可變性)。

為什麼我們需要不可變性?又該如何在高效率與安全性之間取得平衡?


一、 不可變性的起源:為什麼要這麼麻煩?

如果一個物件隨時都能被任何人修改,程式會變得極難預測:

  1. 副作用 (Side Effects):你傳一個物件進去函式,回來後它突然變了,你完全不知道是誰改的。
  2. 比較開銷:要判斷物件是否改變,你必須「深層比對」所有屬性,這效能極差。

不可變性的核心邏輯:如果你改了資料,你就給我一個「全新」的物件。 這樣我只需要比對記憶體位址(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 操作。

  1. 它不會複製整個物件。
  2. 它只會複製「你修改過的那條路徑」。
  3. 沒修改過的分支,新物件會直接指向舊物件的位址(連位址都一樣)。

這就是所謂的 結構共享——既安全又省記憶體。


五、 實戰對比: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 幫你處理那些瑣碎的結構共享。

️ 進階挑戰

  1. 實作挑戰:試著寫一個 deepFreeze 函式,它會遞歸遍歷物件並執行 Object.freeze
  2. 深度思考:在 Vue 中我們擁抱 Proxy 實現的可變性,在 React 中我們擁抱 Immer 實現的不可變性,這兩者的設計哲學有什麼本質區別?哪一個更有利於開發大型應用?

延伸閱讀與資源

返回專題首頁