【実務・中級編】Object.freeze, seal, preventExtensions:オブジェクトの不変性を制御する3つの段階 – JavaScriptコア文法・モダン言語仕様と変数・関数解析バイブル

【JavaScript徹底解剖】`Object.freeze`・`seal`・`preventExtensions`:V8エンジンのメモリ構造から読み解く、真のオブジェクト不変性制御

コードレビューをしていて、コンポーネントの状態管理やAPIクライアントのレスポンスオブジェクトを扱う際に、次のようなコードを見かけることはないだろうか。

// 「とりあえずconstで宣言しておけば安全」という誤った思い込み
const config = {
apiEndpoint: ‘https://api.example.com’,
timeout: 5000
};

// 平然と書き換えられてしまう
config.timeout = 10000;

`const` は「変数再代入の禁止」を強制するものであり、オブジェクトの「内部状態(プロパティ)の不変性(Immutability)」を保証するものでは毛頭ない。V8エンジンは、`const` であろうが `let` であろうが、ヒープメモリ上に展開されたオブジェクトのプロパティ値が書き換えられるのを止めることはない。

現代のフロントエンド開発、特にRedux等の状態管理ライブラリの裏側や、複雑なドメインモデルを扱うコンポーネント設計において、「意図しない変異(Mutation)」は、予測不可能なバグの温床となる。

今回は、JavaScriptが標準で備えるオブジェクトの拡張・変更制御の3つの段階──`Object.preventExtensions`、`Object.seal`、そして `Object.freeze` を、V8の内部挙動(隠匿クラスとインラインキャッシュ)の視点を交えながら、ロジカルかつシャープに解説していこう。

—

1. 3つの段階の全体像とV8エンジンの裏側

JavaScriptのオブジェクトは動的なハッシュマップであり、実行時にプロパティの追加・削除・変更が自由に行える。これが柔軟性の源泉であると同時に、パフォーマンス最適化と予測可能性の敵にもなり得る。

V8エンジンは、プロパティ構造が変化しないオブジェクトに対して「隠匿クラス(Hidden Class / Shape)」を割り当て、メモリアクセスをC言語の構造体並みに最適化している。これら3つのメソッドは、このオブジェクトの「構造」と「値」の可変性を段階的に制限する。

[ Object.preventExtensions ]
└─ プロパティの「追加」を禁止する(削除・変更は可能)

[ Object.seal ] (拡張禁止 + 全プロパティのconfigurable: false化)
└─ プロパティの「追加・削除」を禁止する(値の変更は可能)

[ Object.freeze ] (封印 + 全プロパティのwritable: false化)
└─ プロパティの「追加・削除・変更」のすべてを禁止する(完全な不変性)

それぞれの挙動を、メモリとランタイムの観点から深掘りする。

—

2. 第1段階:`Object.preventExtensions`(拡張の禁止)

概要と挙動

文字通り、そのオブジェクトに対して新しいプロパティの追加を一切禁止する。既存プロパティの削除や、値の書き換えは依然として可能である。

const user = { name: ‘Alice’ };

Object.preventExtensions(user);

// 新規追加は失敗する(厳格モードではTypeError、非厳格モードでは単に無視される)
user.age = 30;
console.log(user.age); // undefined

// 既存プロパティの変更は可能
user.name = ‘Bob’;
console.log(user.name); // ‘Bob’

// 既存プロパティの削除も可能
delete user.name;
console.log(user.name); // undefined

いつ使うのか?

「このオブジェクトのスキーマ(構造)をこれ以上拡張させたくないが、値の更新はパフォーマンスよく行いたい」という限定的なケースで用いる。プラグインシステムなどで、拡張ポイントを固定化したい設計において有効だが、実務のフロントエンド開発で単体で登場する機会は比較的少ない。

—

3. 第2段階:`Object.seal`(封印)

概要と挙動

オブジェクトを「封印」する。これは `preventExtensions` の効果に加え、すべての既存プロパティの `configurable` 属性を `false` に変更する。これにより、プロパティの追加ができなくなるだけでなく、プロパティの削除も不可能になる。

ただし、既存プロパティの `writable` が `true` であれば、値の書き換えは可能である。

const settings = {
theme: ‘dark’,
notifications: true
};

Object.seal(settings);

// 1. 追加不可
settings.autoSave = false;

// 2. 削除不可
delete settings.theme;
console.log(settings.theme); // ‘dark’ (削除されていない)

// 3. 値の変更は可能
settings.theme = ‘light’;
console.log(settings.theme); // ‘light’

実務での活用文脈

状態の「構造」を完全に固定しつつ、中身のプリミティブな値や設定値だけを動的に更新したい場合に適している。Vue 3のリアクティブシステムや古いMVVMフレームワークの内部構造などで、予期せぬプロパティの削除による例外を防ぐ防衛的プログラミングとして機能する。

—

4. 第3段階:`Object.freeze`(凍結・完全不変)

概要と挙動

もっとも厳格な不変性制御。`Object.seal` の効果に加え、すべての既存プロパティの `writable` 属性を `false` に変更する。これにより、プロパートの追加・削除・値の変更のすべてがブロックされる。

const API_CONFIG = {
TIMEOUT: 3000,
ENDPOINTS: {
AUTH: ‘/api/v1/auth’,
USER: ‘/api/v1/user’
}
};

Object.freeze(API_CONFIG);

// すべて失敗する(厳格モードでは例外発生)
API_CONFIG.TIMEOUT = 5000;
API_CONFIG.RETRIES = 3;
delete API_CONFIG.ENDPOINTS;

console.log(API_CONFIG.TIMEOUT); // 3000 (変更されていない)

⚠️ 致命的な注意点:シャロー(浅い)凍結の罠

プログラマブルに最も犯しやすい過ちは、`Object.freeze` がシャロー(浅い)コピーと同様に、ネストされたオブジェクトまで凍結しないという点だ。

上記のコードで、`API_CONFIG.ENDPOINTS` は凍結されていない。

// ネストされたオブジェクトは凍結されていないため、変更可能!
API_CONFIG.ENDPOINTS.AUTH = ‘/api/v2/auth’;
console.log(API_CONFIG.ENDPOINTS.AUTH); // ‘/api/v2/auth’ に書き換わってしまう

この仕様を見落とし、APIの定数定義や設定ファイルを `Object.freeze` したつもりでネストされた値を書き換え、デバッグに数時間を費やすというのは、ジュニアからシニアへのステップアップ過程で誰もが一度は通る通過儀礼である。

—

5. プロダクションコードで使える:ディープフリーズ(Deep Freeze)の実装

実務でオブジェクトの不変性を完全に担保するためには、再帰的にオブジェクトを走査してすべてを凍結するユーティリティ関数(Deep Freeze)を用意し、定数定義や状態の初期値に適用するのがベストプラクティスだ。

以下に、実務のプロダクションコードでそのまま使える堅牢な実装を示す。循環参照(Circular Reference)への耐性も考慮したプロフェッショナルなコードだ。

/