JavaScript | 定数(再代入できない変数)を宣言

JavaScript
スポンサーリンク

概要

  • 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;
JavaScript
  • let/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. 実務での不変性アプローチ(選択肢と長所短所)

  1. Object.freeze(浅い)
    • 使いどころ:小さな設定オブジェクト、公開 API の防御。コスト小。
    • 欠点:ネストは凍らない。
  2. 再帰的 deepFreeze
    • 使いどころ:少量のデータで完全に凍らせたいとき。
    • 欠点:性能コスト、getter 副作用、循環参照ケアが必要。
  3. Proxy(防御ラッパー)
    • 使いどころ:開発中に mutation の検出や例外発生でバグ検出したい時。
    • 欠点:オリジナル参照を防げない・オーバーヘッド。
  4. 不変データ構造(Immutable.js 等)/構造共有(persistent DS)
    • 使いどころ:大規模データの頻繁な更新が必要で、旧状態を効率的に保持したいとき(例:Undo/Redo、差分検出)。
    • 長所:メモリ効率(構造共有)、性能上の利点。
    • 短所:学習コスト、API 違い、既存コードと相互運用の手間。
  5. Immer(推奨パターンのひとつ)
    • ミュータブルなコード(draft.x = 1)を書くだけで内部で効率的に不変な新インスタンスを作る(プロキシベース)。Redux の reducer 等で非常に便利。

例(概念):

// Immer を使うイメージ
import produce from "immer";
const next = produce(base, draft => {
  draft.items.push(1);
});
JavaScript

7. 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 の通常のオブジェクトとして扱われる。

またトップレベル varconst のグローバルオブジェクト反映の違い:

var x = 1;
const y = 2;
console.log(globalThis.x); // 1
console.log(globalThis.y); // undefined  ← const は globalThis のプロパティにならない
JavaScript

8. 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 }
JavaScript
  • readonly(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);
}
JavaScript

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,recv) {
      const v = Reflect.get(target,prop,recv);
      return (typeof v === 'object' && v !== null) ? createImmutableProxy(v) : v;
    }
  });
}
JavaScript

JavaScript | MDN
JavaScript (JS) は軽量でインタープリター型(あるいは実行時コンパイルされる)第一級関数を備えたプログラミング言語です。ウェブページでよく使用されるスクリプト言語として知られ、多くのブラ...
タイトルとURLをコピーしました