JavaScript | 配列・オブジェクト:ネスト構造の扱い – 不変性(イミュータブル)

JavaScript JavaScript
スポンサーリンク

不変性(イミュータブル)とは何か

不変性(イミュータブル)とは「既存のオブジェクトや配列を直接書き換えず、“新しいインスタンス”を作って変更を表現する」考え方です。ここが重要です:直接代入は一見手軽でも、共有参照があると予期せぬ副作用やバグにつながります。イミュータブルにすると「差分検知が安定」「デバッグが容易」「過去状態の再現(タイムトラベル)」が可能になります。

// 破壊的更新(避ける)
state.user.name = "Bob";

// 非破壊更新(推奨)
const next = { ...state, user: { ...state.user, name: "Bob" } };
JavaScript

なぜイミュータブルが必要なのか(副作用と差分検知)

共有参照の問題: 浅いコピーや直参照のまま内側を変更すると、別の変数が同じ参照を持っている場合に同時に値が変わり、バグが隠れやすくなります。

const a = { profile: { city: "Tokyo" } };
const b = a;                 // 同じ参照
b.profile.city = "Osaka";    // a も変わる(意図せず)
JavaScript

差分検知の安定化: UIや状態管理(React、Vuexなど)では「参照が変わったかどうか」で更新を判断することが多いです。新インスタンスを返すことで、軽い比較(===)で確実に変更を検出できます。

// 非破壊更新の結果は参照が変わる
const nextUser = { ...user, name: "Alice" };
user === nextUser; // false(変更を検出しやすい)
JavaScript

浅いコピーと深いコピー(どこで新しくするか)

浅いコピーの原則: 外側だけ新しくし、更新したい“その階層”でもう一段コピーします。内側の参照共有を避けるために、更新箇所まで階層ごとにスプレッドします。

const state = { user: { profile: { city: "Tokyo" } } };
// city まで届く階層でそれぞれコピー
const next = {
  ...state,
  user: {
    ...state.user,
    profile: { ...state.user.profile, city: "Osaka" }
  }
};
JavaScript

深いコピーの選択: 全体を完全に独立させたい、型が複雑(Map/Set/Date など)、循環参照がある場合は structuredClone を使います。毎回の全コピーはコストが高いので、「必要な階層だけ再構築」が実務では基本です。

const deep = structuredClone(state); // 全階層を独立(高コストに注意)
JavaScript

オブジェクトの不変更新パターン(スプレッド/assign)

外側+内側の段階的スプレッド

const nextUser = { ...user, name: "Bob" };
const nextState = { ...state, user: { ...state.user, name: "Bob" } };
JavaScript

ポイント: 目的地までの各階層を必ずコピー。1段でも省くと参照共有が残ります。

欠損に強い受け皿の合成

const next = {
  ...state,
  user: {
    ...(state.user ?? {}),
    profile: { ...(state.user?.profile ?? {}), city: "Osaka" }
  }
};
JavaScript

ポイント: ?? {} で受け皿を用意すると、欠損のある入力でも安全に非破壊更新できます。

fromEntries/entries で変換型の整形

const filtered = Object.fromEntries(
  Object.entries(state.user).filter(([k]) => k !== "password")
);
JavaScript

ポイント: キー・値を同時に扱う整形は entries → map/filter → fromEntries が読みやすい。


配列の不変更新パターン(map/slice/スプレッド)

要素の部分更新(map で置換)

const nextItems = items.map(it =>
  it.sku === "B5" ? { ...it, qty: it.qty + 1 } : it
);
JavaScript

ポイント: 条件一致の要素だけ新インスタンスに置き換え、その他はそのまま返す。

挿入・削除(slice で再構築)

const insertAt = (arr, i, item) => [
  ...arr.slice(0, i), item, ...arr.slice(i)
];
const removeAt = (arr, i) => [
  ...arr.slice(0, i), ...arr.slice(i + 1)
];
JavaScript

ポイント: delete arr[i] は“穴”が残るので使わない。必ず詰め直して新配列を返す。

多段ネストの安全コピー

const table = [[1,2],[3,4]];
const nextTable = table.map(row => row.map(v => (v === 2 ? 99 : v)));
JavaScript

ポイント: 行も列も新しく作り直すことで、どの段にも破壊的変更が混ざらない。


ツールとテクニック(freeze、イミュータブル補助)

Object.freeze の役割と限界

const cfg = Object.freeze({ a: 1, nested: Object.freeze({ b: 2 }) });
cfg.a = 9;            // 無視(非strict)/TypeError(strict)
cfg.nested.b = 3;     // ここまで凍結していれば変更不可
JavaScript

ポイント: freeze は“変更を防ぐ”ためのガード。既存コードの破壊的更新を止められるが、再構築(非破壊更新)には関与しない。浅い凍結だけだと内側は変更できてしまうので、必要なら多段で凍結する。

オプショナルチェーン+null合体で欠損に強化

const qty = order?.items?.[0]?.qty ?? 0;
const nextItems = (order?.items ?? []).map(/* ... */);
JavaScript

ポイント: 読み取りは ?.、既定値は ??。入口でガードすると後続が簡潔になる。

汎用ヘルパー(パス指定で更新)

function setPath(obj, path, value) {
  if (path.length === 0) return value;
  const [head, ...rest] = path;
  const base = Array.isArray(obj) ? [...obj] : { ...(obj ?? {}) };
  base[head] = setPath(obj?.[head], rest, value);
  return base;
}
const next = setPath(state, ["user","profile","city"], "Osaka");
JavaScript

ポイント: 必要な階層だけ新しくし、それ以外は元参照を保つ。性能と安全性のバランスが良い。


よくある落とし穴(重要ポイントの深掘り)

浅いコピーの過信: { ...obj }[...arr] は外側だけ。内側の参照共有を忘れると、思わぬ副作用が発生します。更新する階層で必ずもう一段コピーする。

破壊的メソッドの混入: push, pop, splice, sort, reverse は配列を破壊します。代替としてスプレッド+concatslicetoSortedtoReversed を使う。

const next = [...arr, x];                  // push の代替
const cut  = arr.slice(0, -1);             // pop の代替
const sorted = arr.toSorted((a,b)=>a-b);   // 破壊しないソート(近年の仕様)
JavaScript

JSON 経由の深いコピーの誤用: JSON.parse(JSON.stringify(x)) は関数・undefined・Symbol・Date・Map/Set が失われます。純粋データ以外には使わない。必要なら structuredClone

パフォーマンス無視: 毎回全体をコピーすると重い。変更箇所に限定して“必要な階層のみ再構築”する設計を徹底する。


実践レシピ(すぐ使える非破壊更新)

設定の合成と上書き

const cfg = { ...defaults, ...userCfg, ...runtime }; // 後勝ち合成
JavaScript

ポイント: 欠損は defaults が補い、後段が優先される。最小限の再構築で明快。

明細行のアップサート(なければ追加、あれば更新)

function upsert(items, pred, makeNew, patch) {
  const idx = items.findIndex(pred);
  if (idx < 0) return [...items, makeNew()];
  return items.map((it, i) => i === idx ? { ...it, ...patch(it) } : it);
}
JavaScript

ポイント: 追加も更新も非破壊で扱える汎用パターン。

局所更新の合成(複数箇所を一度に)

const next = {
  ...state,
  user: { ...state.user, name: "Carol" },
  items: state.items.map(it => it.sku === "A1" ? { ...it, qty: 99 } : it)
};
JavaScript

ポイント: 変更箇所ごとに階層をコピーし、同一トランザクション的に合成する。


まとめ

イミュータブルは「直接書き換えず、新しいインスタンスを返す」設計です。核心は、更新したい階層で必ずコピーし直すこと。オブジェクトは階層ごとにスプレッド、配列は mapslice/スプレッドで置換・挿入・削除を非破壊に行う。欠損には ?.?? を併用し、必要なときだけ深いコピー(structuredClone)。破壊的メソッドを避け、性能は“必要な階層のみ再構築”で最適化する。これを徹底すれば、初心者でも複雑なネスト構造を安全に、読みやすく、意図どおりに扱えるようになります。

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