本文 AI 產出,尚未審核

JavaScript 物件操作 – Object.freeze()Object.seal()


簡介

在 JavaScript 中,物件(Object)是最常見的資料結構,幾乎所有程式都會與之互動。
然而,隨著程式規模增大,不受控的物件變更 會成為除錯與維護的頭痛來源。
Object.freeze()Object.seal() 兩個內建方法,提供了「凍結」或「封印」物件的能力,讓開發者可以在執行階段限制物件的結構與內容,從而提升程式的可預測性與安全性。

本篇文章將深入說明這兩個方法的原理、使用方式、常見陷阱與最佳實踐,並提供實務範例,幫助你在日常開發中正確運用 Object.freeze()Object.seal()


核心概念

1. Object.freeze() – 完全凍結物件

  • 定義:凍結後的物件 不能 被新增、刪除或修改屬性值,也不能改變屬性的可列舉性(enumerable)與可寫性(writable)。
  • 深度Object.freeze() 只凍結第一層,若屬性本身是物件,該子物件仍然是可變的(除非自行遞迴凍結)。

範例 1:基礎凍結

const user = {
  name: "Alice",
  age: 25
};

Object.freeze(user);

user.age = 30;           // ❌ 無效,靜默失敗(在嚴格模式下會拋錯)
user.email = "a@b.com"; // ❌ 無法新增屬性
delete user.name;       // ❌ 無法刪除屬性

console.log(user); // { name: 'Alice', age: 25 }

註解:在非嚴格模式下,非法操作會失敗但不會拋錯;在 'use strict' 下會拋出 TypeError

範例 2:深層凍結(遞迴)

function deepFreeze(obj) {
  Object.freeze(obj);
  Object.getOwnPropertyNames(obj).forEach(prop => {
    if (
      obj[prop] !== null &&
      (typeof obj[prop] === "object" || typeof obj[prop] === "function") &&
      !Object.isFrozen(obj[prop])
    ) {
      deepFreeze(obj[prop]); // 递归冻结子对象
    }
  });
  return obj;
}

const config = {
  api: {
    url: "https://api.example.com",
    timeout: 5000
  },
  debug: true
};

deepFreeze(config);

config.api.timeout = 10000; // ❌ 失效
config.debug = false;      // ❌ 失效

註解deepFreeze 會遍歷所有可列舉屬性,將每一層都凍結,適用於需要「不可變」設定檔的情境。

2. Object.seal() – 封印物件

  • 定義:封印後的物件 不能 新增或刪除屬性,但仍允許修改已存在屬性的值(除非屬性本身是 writable: false)。
  • 特性:已封印的物件會自動把所有屬性的 configurable 設為 false,因此屬性的描述子(enumerable、writable)仍可被更改,只是無法再刪除或重新定義屬性。

範例 3:封印後仍可修改值

const point = { x: 0, y: 0 };
Object.seal(point);

point.x = 10;          // ✅ 可以修改已有屬性
point.z = 5;           // ❌ 無法新增屬性
delete point.y;        // ❌ 無法刪除屬性

console.log(point); // { x: 10, y: 0 }

範例 4:封印與屬性描述子

const settings = {
  theme: "light",
  version: 1
};

Object.seal(settings);

// 嘗試把 theme 變成不可寫
Object.defineProperty(settings, "theme", {
  writable: false
}); // ✅ 成功,因為屬性已存在且 configurable 為 false

settings.theme = "dark"; // ❌ 失敗(在嚴格模式會拋錯)

console.log(Object.getOwnPropertyDescriptor(settings, "theme"));
// { value: 'light', writable: false, enumerable: true, configurable: false }

註解Object.seal() 允許在已封印的物件上調整 writableenumerable,但不能把 configurable 再改回 true

3. Object.isFrozen()Object.isSealed()

這兩個方法可用來檢查物件的凍結或封印狀態,常在測試或除錯時使用。

console.log(Object.isFrozen(user));   // true
console.log(Object.isSealed(point)); // true

常見陷阱與最佳實踐

陷阱 說明 解決方式
只凍結第一層 Object.freeze() 只影響第一層屬性,子物件仍可變。 使用遞迴 deepFreeze 或第三方函式庫(如 lodash.freeze)。
忘記使用嚴格模式 非嚴格模式下非法操作會靜默失敗,容易忽略錯誤。 在檔案或函式最前面加入 'use strict';
誤用 seal 期待不可變 seal 仍允許修改屬性值,若需要完全不可變,應使用 freeze 根據需求選擇正確方法;若只想防止新增/刪除,使用 seal
凍結後仍使用 Object.defineProperty configurable: false 後無法重新定義屬性(包括 writable)。 在凍結前先設定好屬性描述子,或避免在凍結後調整。
性能考量 大型物件遞迴凍結會產生額外遍歷成本。 僅在需要保證不變的資料(如設定檔、常量)上使用,或使用 Object.freeze 搭配 Object.assign 建立新物件。

最佳實踐

  1. 明確區分需求

    • 若只想防止 新增/刪除,使用 Object.seal()
    • 若需要 完整不可變,使用 Object.freeze()(必要時遞迴凍結)。
  2. 在模組或常量檔案中一次性凍結

    // constants.js
    export const API_ENDPOINTS = Object.freeze({
      users: "/api/users",
      posts: "/api/posts"
    });
    
  3. 配合 Object.assign 或展開運算子,在不改變原物件的前提下產生新物件:

    const newUser = Object.assign({}, user, { age: 30 });
    // 或
    const newUser = { ...user, age: 30 };
    
  4. 在測試環境驗證不可變性

    test('config should be frozen', () => {
      expect(Object.isFrozen(config)).toBe(true);
    });
    

實際應用場景

  1. 設定檔與常量

    • 將 API URL、環境變數、UI 文字等不可變資料凍結,防止程式意外改寫。
  2. Redux / Flux 狀態管理

    • 雖然 Redux 建議使用純函式返回新狀態,但在開發階段可用 Object.freeze(state) 來檢測不小心的突變(middleware redux-freeze)。
  3. 第三方函式庫的 API

    • 若要提供只讀的物件介面給使用者,先 sealfreeze,確保外部無法改寫內部實作。
  4. 安全性敏感的資料

    • 如使用者權限表、加密金鑰等,凍結後即使被惡意程式取得,也無法在記憶體中被更改。
  5. 防止記憶體洩漏

    • 在大型物件完成使用後凍結,配合垃圾回收機制,可減少意外的持續參照。

總結

  • Object.freeze() 凍結物件,使其 不可變(包括新增、刪除、修改屬性)。
  • Object.seal() 封印物件,使其 不可新增/刪除,但仍允許修改已有屬性的值。
  • 兩者皆只影響第一層,若需深層不可變,必須自行遞迴凍結。
  • 使用嚴格模式、Object.isFrozen() / Object.isSealed() 以及適當的測試,可有效避免因意外變更導致的 bug。
  • 在設定檔、常量、狀態管理與安全性資料等實務情境中,善用這兩個 API 能提升程式碼的可預測性與維護性。

掌握了 Object.freeze()Object.seal(),你就能在 JavaScript 開發中更好地控制資料的生命週期,寫出更安全、更可靠的程式碼。祝你開發順利!