JavaScript | ES6+ 文法:変数・宣言の進化 – 定数オブジェクトの注意点

JavaScript
スポンサーリンク

定数オブジェクトとは何か

const で宣言したオブジェクトは「変数がどのオブジェクトを指すか」が固定されます。ここが重要です:const は参照を固定するだけで、中身(プロパティや配列要素)は変更可能です。完全に不変にしたいなら“凍結”など別の手段が必要です。

const user = { name: "Alice" };
user.name = "Bob";         // OK(中身の変更)
/* user = { name: "Carol" }; */ // NG(別参照への再代入)
JavaScript

参照固定と中身の変更(誤解しやすいポイント)

const は「参照が固定」

オブジェクトや配列の中身はミューテーションできます。値そのものを不変にしたいときは、非破壊更新(新インスタンスを作る)か凍結を使います。

const list = [1, 2];
list.push(3);           // OK(中身の変更)
/* list = []; */        // NG(再代入)
JavaScript

非破壊更新の基本形

UI状態や共有データでは“新インスタンス”で返すのが安全です。

const user = { id: 1, name: "Alice" };
const next = { ...user, name: "Bob" };  // 新インスタンス(元は保つ)
JavaScript

凍結(不変化)の範囲と限界

Object.freeze は“浅い凍結”

直下プロパティを凍結します。ネストの内側はそのままです。ここが重要です:深い構造では浅い凍結だけだと変更を防げません。

const cfg = Object.freeze({ a: 1, nested: { b: 2 } });
// cfg.a = 9;           // 無効(厳格モードなら例外)
cfg.nested.b = 3;       // 変更できてしまう(浅い凍結)
JavaScript

深い凍結が必要なら再帰

用途を絞って使いましょう(コストが高いから)。

function deepFreeze(obj) {
  if (obj && typeof obj === "object") {
    for (const k of Object.getOwnPropertyNames(obj)) deepFreeze(obj[k]);
    return Object.freeze(obj);
  }
  return obj;
}
const cfg = deepFreeze({ a: 1, nested: { b: 2 } });
JavaScript

破壊的メソッドの落とし穴(const でも中身は壊れる)

配列の破壊的メソッドは使えるが危険

sort, reverse, splice, push, pop, shift, unshift は“中身を直接変更”します。共有状態で使うと副作用になります。ここが重要です:UIや共有データでは非破壊版を優先。

const rows = [3, 1, 2];
// 悪い(破壊的)
rows.sort((a,b)=>a-b);         // rows が書き換わる

// 良い(非破壊)
const sorted = rows.toSorted((a,b)=>a-b);  // rows は保つ
const spliced = rows.toSpliced(1, 1);      // 安全な削除
const reversed = rows.toReversed();        // 安全な反転
JavaScript

オブジェクトの直接変更も副作用

const でもプロパティ代入・delete は可能です。共有参照があるなら非破壊で。

const obj = { a: 1, b: 2 };
// 悪い:共有参照を壊す
obj.b = 3;

// 良い:非破壊
const next = { ...obj, b: 3 };
JavaScript

設計で防ぐ(定数オブジェクトの安全運用)

“容器は const、中身は非破壊”が基本

参照のライフサイクルを安定させ、差分検知(===)やキャッシュが効くようにします。

const users = [{ id: 1, active: false }, { id: 2, active: true }];
const nextUsers = users.map(u => u.id === 1 ? { ...u, active: true } : u);
// users は保ち、nextUsers は新参照
JavaScript

モジュール定数は凍結して守る

設定や定義テーブルは凍結して“誤更新”を防止。

export const ROLES = Object.freeze({
  admin: "ADMIN",
  user:  "USER",
});
JavaScript

境界で防御的コピー

外部から受け取ったオブジェクトはコピーして内部に取り込むと安全です(参照共有の副作用を遮断)。

function register(raw) {
  const input = { ...raw };             // 浅コピー
  input.name = (input.name ?? "").trim();
  // …
}
JavaScript

浅コピーの限界と注意(ネストの参照共有)

スプレッドは1階層だけ新しくなる

ネスト内は共有のままです。必要階層だけ“段ごとにコピー”しましょう。ここが重要です:一段でも省くと副作用が漏れます。

const state = { user: { flags: { vip: false } } };
// 良い:必要階層のみコピー
const next = { 
  ...state, 
  user: { ...state.user, flags: { ...state.user.flags, vip: true } }
};
JavaScript

パフォーマンスの現実(凍結・コピーのコスト)

凍結は読み取り中心の構造にだけ

大量・深い構造を頻繁に凍結するとコスト増。“設定定数”“辞書キー”など限定的に使うのが現実的です。

コピーは“必要最小限”

毎回全体をコピーすると割り当て・GCが増えます。更新が必要な部分だけ再構築し、他は参照を保つ設計にします。


例題で理解を固める

定数の設定表を安全に保つ

const TAX_RATE = 0.1;                      // 数値は完全不変
const CATEGORIES = Object.freeze({         // 表は浅い不変で十分なことが多い
  book: "BOOK",
  food: "FOOD",
  misc: "MISC"
});
JavaScript

共有配列を壊さずに並び替える

function sortByPrice(rows) {
  return rows.toSorted((a,b)=>a.price - b.price);  // 非破壊
}
JavaScript

受け取った入力を安全に整形

function shapeUser(raw) {
  const user = { ...raw };                     // 浅コピーで境界防御
  user.name = (user.name ?? "").trim();
  user.age = Number.parseInt(user.age, 10) || null;
  return user;
}
JavaScript

まとめ

定数オブジェクトの核心は「const は参照固定、内側は変更可能」という事実です。完全不変が必要なら凍結(浅い)や深い凍結を使い、通常は“容器は const、中身は非破壊更新”で安全と性能を両立する。配列の破壊的メソッドは避け、toSorted/toSpliced/toReversed を使う。浅コピーの限界を理解し、必要階層だけコピーする。モジュール定数は凍結、外部入力は境界でコピー。この指針に従えば、初心者でも定数オブジェクトを正しく使いこなし、意図どおりの安全な ES6+ コードを書けます。

タイトルとURLをコピーしました