不変性(イミュータブル)とは何か
不変性(イミュータブル)とは「既存のオブジェクトや配列を直接書き換えず、“新しいインスタンス”を作って変更を表現する」考え方です。ここが重要です:直接代入は一見手軽でも、共有参照があると予期せぬ副作用やバグにつながります。イミュータブルにすると「差分検知が安定」「デバッグが容易」「過去状態の再現(タイムトラベル)」が可能になります。
// 破壊的更新(避ける)
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 は配列を破壊します。代替としてスプレッド+concat/slice/toSorted/toReversed を使う。
const next = [...arr, x]; // push の代替
const cut = arr.slice(0, -1); // pop の代替
const sorted = arr.toSorted((a,b)=>a-b); // 破壊しないソート(近年の仕様)
JavaScriptJSON 経由の深いコピーの誤用: 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ポイント: 変更箇所ごとに階層をコピーし、同一トランザクション的に合成する。
まとめ
イミュータブルは「直接書き換えず、新しいインスタンスを返す」設計です。核心は、更新したい階層で必ずコピーし直すこと。オブジェクトは階層ごとにスプレッド、配列は map/slice/スプレッドで置換・挿入・削除を非破壊に行う。欠損には ?.+?? を併用し、必要なときだけ深いコピー(structuredClone)。破壊的メソッドを避け、性能は“必要な階層のみ再構築”で最適化する。これを徹底すれば、初心者でも複雑なネスト構造を安全に、読みやすく、意図どおりに扱えるようになります。
