本文 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()允許在已封印的物件上調整writable、enumerable,但不能把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 建立新物件。 |
最佳實踐
明確區分需求:
- 若只想防止 新增/刪除,使用
Object.seal()。 - 若需要 完整不可變,使用
Object.freeze()(必要時遞迴凍結)。
- 若只想防止 新增/刪除,使用
在模組或常量檔案中一次性凍結:
// constants.js export const API_ENDPOINTS = Object.freeze({ users: "/api/users", posts: "/api/posts" });配合
Object.assign或展開運算子,在不改變原物件的前提下產生新物件:const newUser = Object.assign({}, user, { age: 30 }); // 或 const newUser = { ...user, age: 30 };在測試環境驗證不可變性:
test('config should be frozen', () => { expect(Object.isFrozen(config)).toBe(true); });
實際應用場景
設定檔與常量
- 將 API URL、環境變數、UI 文字等不可變資料凍結,防止程式意外改寫。
Redux / Flux 狀態管理
- 雖然 Redux 建議使用純函式返回新狀態,但在開發階段可用
Object.freeze(state)來檢測不小心的突變(middlewareredux-freeze)。
- 雖然 Redux 建議使用純函式返回新狀態,但在開發階段可用
第三方函式庫的 API
- 若要提供只讀的物件介面給使用者,先
seal再freeze,確保外部無法改寫內部實作。
- 若要提供只讀的物件介面給使用者,先
安全性敏感的資料
- 如使用者權限表、加密金鑰等,凍結後即使被惡意程式取得,也無法在記憶體中被更改。
防止記憶體洩漏
- 在大型物件完成使用後凍結,配合垃圾回收機制,可減少意外的持續參照。
總結
Object.freeze()凍結物件,使其 不可變(包括新增、刪除、修改屬性)。Object.seal()封印物件,使其 不可新增/刪除,但仍允許修改已有屬性的值。- 兩者皆只影響第一層,若需深層不可變,必須自行遞迴凍結。
- 使用嚴格模式、
Object.isFrozen()/Object.isSealed()以及適當的測試,可有效避免因意外變更導致的 bug。 - 在設定檔、常量、狀態管理與安全性資料等實務情境中,善用這兩個 API 能提升程式碼的可預測性與維護性。
掌握了 Object.freeze() 與 Object.seal(),你就能在 JavaScript 開發中更好地控制資料的生命週期,寫出更安全、更可靠的程式碼。祝你開發順利!