こんにちは!JavaScriptのコアな仕組みからモダンな開発まで、日夜コードと向き合っているフロントエンド・アーキテクトです。
今回は、JavaScriptのデータ型、特に「オブジェクトの不変性(イミュータビリティ)」をコントロールする3つの強力なメソッド、`Object.preventExtensions`、`Object.seal`、`Object.freeze`について深く掘り下げていきます。
状態管理ライブラリ(ReduxやZustandなど)の裏側でも、意図しないデータの書き換えを防ぐために、こうしたオブジェクトの保護機構が巧みに使われています。「なぜデータの書き換えが怖いのか」「どうやって防ぐのか」を、V8エンジンのメモリ管理の気配を感じつつ、一緒に紐解いていきましょう。ここをクリアすれば、JavaScriptのデータ構造に対する理解が一段と深まりますよ!
—
1. なぜJavaScriptのオブジェクトは「危険」なのか?
JavaScriptのオブジェクトは、非常に柔軟です。プログラミングの実行中(ランタイム)に、いつでも自由に新しいプロパティを追加したり、既存の値を書き換えたり、削除したりできます。
const user = { name: ‘Taro’ };
// 後から勝手にプロパティを追加できる
user.age = 25;
// 勝手に値を書き換えられる
user.name = ‘Jiro’;
// 勝手にプロパティを削除できる
delete user.age;
この「何でもアリ」な動的柔軟性がJavaScriptの魅力である一方、大規模なアプリケーション開発ではバグの温床になります。「知らないうちに別の関数でオブジェクトを書き換えられていた(ミューテーションの弊害)」という問題に直面したことがある方も多いのではないでしょうか。
そこで登場するのが、オブジェクトの拡張や変更をロックする3つの段階です。
—
2. 不変性をコントロールする3つのレベル
JavaScriptでは、オブジェクトの「堅牢さ(守りの強さ)」を以下の3段階で厳密に制御できます。
1. `Object.preventExtensions()` (拡張禁止:新築禁止)
2. `Object.seal()` (密閉:リフォーム・増改築禁止)
3. `Object.freeze()` (凍結:完全な読み取り専用)
それぞれの仕組みを、家のリフォームに例えながら見ていきましょう。
—
レベル1:`Object.preventExtensions()` (拡張禁止)
最初に紹介するのは、オブジェクトに「新しいプロパティを追加すること」だけを禁止するメソッドです。
- できること: 既存プロパティの値の変更、既存プロパティの削除
- できないこと: 新しいプロパティの追加
// 物件(オブジェクト)を新築する
const house = {
roof: ‘Red’,
walls: ‘White’
};
// これ以上「新しい部屋(プロパティ)」の増築を禁止する
Object.preventExtensions(house);
// 1. 既存の壁の色を塗り替える(値の変更) -> OK!
house.walls = ‘Blue’;
// 2. 既存の屋根を壊す(プロパティの削除) -> OK!
delete house.roof;
// 3. 新しいガレージを追加する(プロパティの追加) -> NG!(厳格モードではエラー、非厳格モードでは無視される)
house.garage = ‘Big’;
console.log(house);
// 出力結果: { walls: ‘Blue’ } (roofは消え、garageは追加されていない)
【エンジニアの知見】
V8エンジンの内部(Hidden Classの概念)において、このメソッドはオブジェクトの形状(Shape)の動的な拡張をストップさせます。「これ以上メモリ上の構造を変化させない」という最初のブレーキになります。
—
レベル2:`Object.seal()` (密閉)
次は、オブジェクトを「密閉」するメソッドです。先ほどの「拡張禁止」に加えて、「プロパティの削除」も禁止します。
- できること: 既存プロパティの値の変更
- できないこと: 新しいプロパティの追加、既存プロパティの削除、プロパティの属性(設定)変更
家で言えば、「壁紙の張り替え(値の変更)はできるけれど、柱を抜けたり(削除)、部屋を新しく増やす(追加)ことは一切できない」状態です。
const smartphone = {
model: ‘iPhone’,
storage: ‘128GB’
};
// 密閉する
Object.seal(smartphone);
// 1. ストレージの容量を書き換える -> OK!
smartphone.storage = ‘256GB’;
// 2. 新しい機能(カメラなど)を追加する -> NG!
smartphone.camera = ‘Triple’;
// 3. 型番(model)を削除する -> NG!
delete smartphone.model;
console.log(smartphone);
// 出力結果: { model: ‘iPhone’, storage: ‘256GB’ }
【ここで注意!重要ポイント】
`Object.seal()` を使っても、既存プロパティの「値」の書き換えは防げません。 値さえ変えられればいいのか、それとも構造自体を守りたいのかを意識して使い分ける必要があります。
—
レベル3:`Object.freeze()` (凍結)
最後が、最も強力な「オブジェクトの凍結(Freeze)」です。状態管理や定数オブジェクトの定義で最もよく使われます。
- できること: 読み取り(値の参照)のみ
- できないこと: 新しいプロパティの追加、既存プロパティの削除、既存プロパティの値の変更、プロパティの属性変更
完全にカチコチに凍りついた氷の彫刻のようなもので、一切の改変を受け付けなくなります。
const config = {
API_ENDPOINT: ‘https://api.example.com’,
TIMEOUT: 5000
};
// 完全凍結!
Object.freeze(config);
// 1. 値を書き換えようとする -> NG!(厳格モードでは TypeError が発生)
config.TIMEOUT = 10000;
// 2. 新しい設定を追加する -> NG!
config.RETRY_COUNT = 3;
// 3. 既存の設定を削除する -> NG!
delete config.API_ENDPOINT;
console.log(config);
// 出力結果: { API_ENDPOINT: ‘https://api.example.com’, TIMEOUT: 5000 }
// (値は一切変わっていない!)
—
3. 実践:厳密な型判定と「浅い凍結(Shallow Freeze)」の罠
ここで、初学者が絶対にハマる非常に重要な罠についてお話しします。
実は、`Object.freeze()` は「浅い凍結(Shallow Freeze)」を行います。つまり、オブジェクトの「一段階目の階層」しか凍結してくれません。オブジェクトの中にさらに別のオブジェクト(ネスト構造)がある場合、内側のオブジェクトは凍結されずに書き換えられてしまいます。
const userProfile = {
name: ‘Ken’,
settings: {
theme: ‘dark’
}
};
Object.freeze(userProfile);
// 一段階目は凍結されているので、名前は変えられない
userProfile.name = ‘Shin’;
// しかし!二段階目のオブジェクト(settings)の中身は凍結されていない!
userProfile.settings.theme = ‘light’;
console.log(userProfile.settings.theme);
// 出力結果: ‘light’ (書き換わってしまう!)
完全な不変性(Deep Freeze)を実現するには?
実務で本当に堅牢なオブジェクトを作りたい場合は、再帰処理(リカージョン)を使って、内側のオブジェクトまでくまなく凍結させるユーティリティ関数を書く必要があります。
// オブジェクトを再帰的に完全凍結する関数
function deepFreeze(obj) {
// オブジェクトのプロパティ名をすべて取得する
const propNames = Object.getOwnPropertyNames(obj);
// すべてのプロパティに対して再帰的に freeze を実行
propNames.forEach((name) => {
const prop = obj[name];
// プロパティがオブジェクト(かつ null でない)場合、再帰呼び出し
if (prop && typeof prop === ‘object’) {
deepFreeze(prop);
}
});
return Object.freeze(obj);
}
// 使ってみる
const secureConfig = deepFreeze({
app: {
name: ‘SuperApp’,
version: ‘1.0.0’
}
});
// これで内側も完全に守られます!
// secureConfig.app.version = ‘2.0.0’; // エラーまたは無視される
—
4. まとめ:3つのメソッドの比較表
最後に、今回学んだ3つのメソッドの特徴をすっきりと整理しておきましょう。
| メソッド | 新規追加 | 既存削除 | 値の変更 | 主なユースケース |
| :— | :—: | :—: | :—: | :— |
| `Object.preventExtensions()` | ❌ 不可 | ⭕ 可能 | ⭕ 可能 | 構造をこれ以上増やしたくない時 |
| `Object.seal()` | ❌ 不可 | ❌ 不可 | ⭕ 可能 | データの「形」を固定しつつ中身だけ更新したい時 |
| `Object.freeze()` | ❌ 不可 | ❌ 不可 | ❌ 不可 | 定数定義や、絶対に書き換えられたくない状態管理 |
JavaScriptのオブジェクト指向や関数型プログラミングにおいて、「データをむやみに書き換えない(イミュータビリティ)」というアプローチは、予測可能性の高いバグの少ないコードを書くための強力な武器になります。
「ここをこう保護したいから `freeze` を使おう」「いや、内側があるから `deepFreeze` にしよう」と、メモリやデータの流れを意識しながらコードをデザインできるようになれば、あなたもう立派なJavaScriptマスターです。
今日の学びを、ぜひ実際の開発やデバッグの現場で活かしてくださいね!