概要
JavaScriptではオブジェクトの変更がデフォルトで許可されているが、**意図せぬミューテーション(変更)**は予期せぬバグの温床になる。
この問題を制御するために導入されるのが、不変性(immutability)の概念。
Object.freeze()
を使うことでオブジェクトをミューテーション不可能にできるが、実はこれには浅い制約しか与えられない。
本稿では以下の観点から、不変性を“設計”として導入する方法を提示する:
-
Object.freeze()
の正しい理解と限界 - 深いネスト構造における凍結の再帰処理
- 状態管理における構造共有戦略(Structural Sharing)
- ライブラリを用いた実用的な不変性管理(Immer, Immutable.js)
Object.freeze() の基本挙動
const obj = {
name: 'Toto'
};
Object.freeze(obj);
obj.name = 'Changed'; // ❌ 無視される(strictならエラー)
console.log(obj.name); // 'Toto'
- ✅ プロパティの変更・追加・削除が不可
- ✅
Object.isFrozen(obj)
で凍結状態を判定可能 - ❌ ネストされたオブジェクトは対象外(浅い凍結)
浅い vs 深い凍結
const deep = {
outer: {
inner: 'value'
}
};
Object.freeze(deep);
deep.outer.inner = 'changed'; // ✅ 変更される(freezeの対象外)
→ ✅ 深い構造では再帰的に凍結する必要あり
再帰的なディープフリーズの実装
function deepFreeze(obj) {
Object.freeze(obj);
for (const key of Object.keys(obj)) {
const val = obj[key];
if (typeof val === 'object' && val !== null && !Object.isFrozen(val)) {
deepFreeze(val);
}
}
return obj;
}
→ ✅ オブジェクト全体を凍結する安全な関数
不変性の設計的メリット
効果 | 説明 |
---|---|
副作用の制御 | 状態が変更不可 → 安全な関数設計が可能 |
デバッグ容易 | どこかで書き換えられた問題が原理的に起きない |
パフォーマンス最適化 | 構造共有により shallow compare が可能 → React等で有効 |
テストの一貫性 | オブジェクトが状態を変えないため、再現性の高いテスト設計が可能 |
状態管理における構造共有戦略(Structural Sharing)
const state1 = { a: 1, b: 2 };
const state2 = { ...state1, b: 3 };
console.log(state1 === state2); // false
console.log(state1.a === state2.a); // true(構造共有)
→ ✅ ミューテーションせず、新しいオブジェクトを作る
→ ✅ React, Redux, Vuex などでの変更検知に最適
ライブラリ活用:Immerによる不変性の“開発体験改善”
import { produce } from 'immer';
const base = { count: 0 };
const next = produce(base, draft => {
draft.count += 1;
});
console.log(base.count); // 0
console.log(next.count); // 1
- ✅
draft
を書き換えるように書いても、元オブジェクトは不変 - ✅ 内部的に構造比較+差分追跡 → 効率的な新状態を生成
Immutable.jsによる完全な不変コレクション
import { Map } from 'immutable';
const map1 = Map({ a: 1, b: 2 });
const map2 = map1.set('b', 3);
console.log(map1.get('b')); // 2
console.log(map2.get('b')); // 3
- ✅ 内部的に構造共有された不変コレクション
- ❌ 学習コストと専用メソッドの多さがデメリット
設計判断フロー
① 明示的に変更をブロックしたい? → Object.freeze
② 深い構造も含めて凍結したい? → deepFreeze関数を導入
③ 状態のミューテーションを避けつつ操作性を保ちたい? → Immer
④ 大規模な不変データ管理が必要? → Immutable.js
⑤ リアクティブな変更検知を最適化したい? → 構造共有の実装
よくある誤解
❌ Object.freeze() で全ての変更がブロックされる?
→ ❌ ネストされたオブジェクトはそのまま変更可能
→ ✅ deepFreeze()
を使う or ライブラリ利用が安全
❌ Object.freeze() はセキュリティ機構?
→ ❌ いいえ、設計的ガードであり、完全な秘匿化ではない
→ プロパティは Object.getOwnPropertyDescriptors()
で依然可視
結語
Object.freeze()
は、単なる構文ではない。
それは「データが書き換わらないという保証」を設計上に持ち込むための契約である。
- ミューテーションの副作用からロジックを解放し、
- 状態の変化を構造として明確に定義し、
- アプリケーションの一貫性と予測性を高める
不変性とは、変わらないという“前提”をコードに刻むことで、信頼できる設計を作る技法である。