- 概要
- 1. TDZ とホイスティングの深い理解
- 2. binding と値(参照)の区別:プリミティブ vs 参照型
- 3. Object.preventExtensions / seal / freeze の違い
- 4. 再帰的(深い)freeze の実装例(循環参照に注意)
- 5. Proxy を使った「防御的イミュータブルラッパー」
- 6. 実務での不変性アプローチ(選択肢と長所短所)
- 7. const と ES Modules(import/export)の微妙な点
- 8. const と分割代入(destructuring)
- 9. TypeScript の as const と readonly(型レベルの不変)
- 10. 実務でのベストプラクティス
- 11. 参考スニペット(まとめ)
概要
constは「その識別子が別の値を指すように再バインドできない」という束縛(binding)の不変性を与えるだけで、参照先オブジェクトそのものが不変になるわけではありません。- 実際の「オブジェクトの不変化」をしたければ
Object.freeze/ 再帰的 freeze / Proxy / 外部ライブラリ(Immer, Immutable.js 等)を使う、という選択になります。
1. TDZ とホイスティングの深い理解
console.log(a); // undefined
var a = 1;
console.log(b); // ReferenceError: Cannot access 'b' before initialization (TDZ)
let b = 2;
console.log(c); // ReferenceError: Cannot access 'c' before initialization (TDZ)
const c = 3;
JavaScriptlet/constは宣言位置までは TDZ(Temporal Dead Zone)にある:実際には「ホイスト」されるが初期化されないため、宣言より前にアクセスするとReferenceError。function宣言は実行時以前に初期化される(=呼び出せる)が、const f = function(){}のような代入式は TDZ の影響を受ける(宣言前に呼べない)。
2. binding と値(参照)の区別:プリミティブ vs 参照型
const n = 10;
n = 20; // TypeError
const obj = { a: 1 };
obj.a = 2; // OK — オブジェクトの内部は変更できる
obj = {}; // TypeError — 識別子 obj を別オブジェクトへ再バインドできない
JavaScript重要:const は「名前(識別子)を別の値へ向け直せない」だけ。中身は変えられる(参照先が可変なら可変)。
3. Object.preventExtensions / seal / freeze の違い
Object.preventExtensions(obj):新しいプロパティの追加を禁止(既存プロパティはそのまま)Object.seal(obj):新規追加と削除を禁止(既存プロパティは non-configurable になるが writable は維持)Object.freeze(obj):sealに加えてすべてのデータプロパティをwritable: falseにする(浅い凍結)
例:
const o = { a: 1 };
Object.freeze(o);
o.a = 2; // strict modeだと TypeError、非 strict だと無視される(書き換わらない)
JavaScript注意点:
freezeしてもプロトタイプチェーン上のオブジェクトは凍らない。freezeは浅い(shallow):o.nestedがオブジェクトならo.nested.some = 1は可能。
4. 再帰的(深い)freeze の実装例(循環参照に注意)
循環参照を防ぐために WeakSet を使う実装の例:
function deepFreeze(obj, seen = new WeakSet()) {
if (obj === null) return obj;
if (typeof obj !== 'object' && typeof obj !== 'function') return obj;
if (seen.has(obj)) return obj;
seen.add(obj);
// own property names + symbols
const keys = Object.getOwnPropertyNames(obj).concat(Object.getOwnPropertySymbols(obj));
for (const key of keys) {
const desc = Object.getOwnPropertyDescriptor(obj, key);
// 値がオブジェクトだったら再帰(getter を呼ぶと副作用が起きる可能性がある点に注意)
if (desc && 'value' in desc) {
deepFreeze(desc.value, seen);
}
}
return Object.freeze(obj);
}
JavaScript注意点:
- 上の実装は アクセッサ(getter)を呼ばない方針(
desc.valueを使う)。もしobj[prop]を参照して値を得る実装にすると getter が実行され副作用が走る可能性あり。 WeakSetによって循環参照でも安全に走査できる。
5. Proxy を使った「防御的イミュータブルラッパー」
Object.freeze と違い、Proxy は「見かけ上の不変性」を提供でき、操作時に例外を投げるなど柔軟に振る舞えます。ただし元のオブジェクトが別の参照から変更可能だと、Proxy を経由しない変更は防げません(ラッパー方式の限界)。
function createImmutableProxy(obj) {
if (obj === null || typeof obj !== 'object') return obj;
return new Proxy(obj, {
set() { throw new Error('Cannot modify immutable object'); },
deleteProperty() { throw new Error('Cannot delete property'); },
get(target, prop, receiver) {
const val = Reflect.get(target, prop, receiver);
return (typeof val === 'object' && val !== null) ? createImmutableProxy(val) : val;
}
});
}
JavaScript長所:エラーを投げてバグを早期検出できる。短所:参照の共有を完全には制御できない・性能コスト。
6. 実務での不変性アプローチ(選択肢と長所短所)
Object.freeze(浅い)- 使いどころ:小さな設定オブジェクト、公開 API の防御。コスト小。
- 欠点:ネストは凍らない。
- 再帰的 deepFreeze
- 使いどころ:少量のデータで完全に凍らせたいとき。
- 欠点:性能コスト、getter 副作用、循環参照ケアが必要。
- Proxy(防御ラッパー)
- 使いどころ:開発中に mutation の検出や例外発生でバグ検出したい時。
- 欠点:オリジナル参照を防げない・オーバーヘッド。
- 不変データ構造(Immutable.js 等)/構造共有(persistent DS)
- 使いどころ:大規模データの頻繁な更新が必要で、旧状態を効率的に保持したいとき(例:Undo/Redo、差分検出)。
- 長所:メモリ効率(構造共有)、性能上の利点。
- 短所:学習コスト、API 違い、既存コードと相互運用の手間。
- Immer(推奨パターンのひとつ)
- ミュータブルなコード(
draft.x = 1)を書くだけで内部で効率的に不変な新インスタンスを作る(プロキシベース)。Redux の reducer 等で非常に便利。
- ミュータブルなコード(
例(概念):
// Immer を使うイメージ
import produce from "immer";
const next = produce(base, draft => {
draft.items.push(1);
});
JavaScript7. const と ES Modules(import/export)の微妙な点
// a.js
export const cfg = { mode: 'dev' };
// b.js
import { cfg } from './a.js';
cfg = {}; // SyntaxError: imported binding read-only
cfg.mode = 'prod'; // OK — cfg 自体は再代入不可だがプロパティはミュータブル
JavaScript- モジュールの
exportは「ライブな読み取り専用バインディング」として動作(再代入できない)。しかし、エクスポートされたオブジェクトの内部は JS の通常のオブジェクトとして扱われる。
またトップレベル var と const のグローバルオブジェクト反映の違い:
var x = 1;
const y = 2;
console.log(globalThis.x); // 1
console.log(globalThis.y); // undefined ← const は globalThis のプロパティにならない
JavaScript8. const と分割代入(destructuring)
- 各識別子は必ず初期化される必要がある(
constのルール)。
const { a, b = 2 } = { a: 1 }; // a=1, b=2
const [x, ...rest] = [1,2,3];
x = 5; // TypeError: Assignment to constant variable.
JavaScript- 分割代入でネストしたオブジェクトや既定値を使う場合も、各変数は一度だけ束縛される。
9. TypeScript の as const と readonly(型レベルの不変)
as const(const assertion)は型をリテラルに狭め、readonly にする。ランタイムのObject.freezeを伴わないため「型安全」はあるが実行時の凍結はされない点に注意。
const arr = [1,2] as const;
// 型: readonly [1, 2]
// arr[0] = 3; // コンパイルエラー(readonly)
const obj = { a: 1 } as const;
// 型: { readonly a: 1 }
JavaScriptreadonly(TS)とconst(JS)は役割が異なる:readonlyは型レベルの制約、constはランタイムの再バインド不可。
10. 実務でのベストプラクティス
- デフォルトは
constを使う(「まずconst、必要ならlet」)。コードの意図が明確になりバグ減。 - 不変性が設計上重要なら 浅い freeze は早期防御に使い、深い不変性は Immer や Immutable の導入を検討する。
- 公開 API や設定オブジェクトは
Object.freeze(少量)で防御し、性能に注意。 - TypeScript を使うなら型安全のため
as const/readonlyを活用(ただし実行時の保証は別)。
11. 参考スニペット(まとめ)
深い凍結(循環対応)
function deepFreeze(obj, seen = new WeakSet()) {
if (obj === null) return obj;
if (typeof obj !== 'object' && typeof obj !== 'function') return obj;
if (seen.has(obj)) return obj;
seen.add(obj);
const keys = Object.getOwnPropertyNames(obj).concat(Object.getOwnPropertySymbols(obj));
for (const k of keys) {
const desc = Object.getOwnPropertyDescriptor(obj, k);
if (desc && 'value' in desc) deepFreeze(desc.value, seen); // getter を呼ばない実装
}
return Object.freeze(obj);
}
JavaScriptProxy ベースの防御ラッパー(例)
function createImmutableProxy(obj) {
if (obj === null || typeof obj !== 'object') return obj;
return new Proxy(obj, {
set() { throw new Error('Cannot modify immutable object'); },
deleteProperty() { throw new Error('Cannot delete property'); },
get(target,prop,recv) {
const v = Reflect.get(target,prop,recv);
return (typeof v === 'object' && v !== null) ? createImmutableProxy(v) : v;
}
});
}
JavaScriptJavaScript | MDN
JavaScript (JS) は軽量でインタープリター型(あるいは実行時コンパイルされる)第一級関数を備えたプログラミング言語です。ウェブページでよく使用されるスクリプト言語として知られ、多くのブラ...
