JavaScript | 配列・オブジェクト:ネスト構造の扱い – 深い階層の更新

JavaScript JavaScript
スポンサーリンク

深い階層の更新とは何か

深い階層の更新は「入れ子(ネスト)になったオブジェクトや配列の、何層も奥にある値を安全に書き換えること」です。ここが重要です:共有状態(UIやストア)では“非破壊更新(新しいオブジェクトを返す)”が基本。浅いコピーは外側だけ独立し、内側は参照共有なので“更新したい階層でさらにコピー”するのが定石です。

const state = {
  user: { name: "Alice", profile: { city: "Tokyo", zip: "100-0001" } },
  items: [{ sku: "A1", qty: 2 }, { sku: "B5", qty: 1 }]
};
JavaScript

オブジェクトの深い更新(階層ごとにスプレッド)

基本パターン(外→中→目的地の順でコピー)

更新したい“階層すべて”でスプレッドして新インスタンスを作ります。これが最も読みやすく、破壊的変更を避けられる王道です。

// city を "Osaka" に変更(非破壊)
const next = {
  ...state,
  user: {
    ...state.user,
    profile: {
      ...state.user.profile,
      city: "Osaka"
    }
  }
};
JavaScript

ここが重要です:一段でもスプレッドを省くと、その階層は元の参照を共有したままになり、別の場所で“予期せず同時に変わる”バグの原因になります。

存在しない中間階層を“作りながら更新”

欠損しているパスにも安全に書き込むには、受け皿を用意しつつ合成します。

// state.user.profile が無い可能性がある
const next = {
  ...state,
  user: {
    ...(state.user ?? {}),
    profile: {
      ...(state.user?.profile ?? {}),
      city: "Osaka"
    }
  }
};
JavaScript

配列の深い更新(要素の置換・挿入・削除)

特定要素の部分更新(map+条件分岐)

配列要素を非破壊で変えるときは map を使い、該当要素だけスプレッドで置き換えます。

// sku === "B5" の行だけ qty を +1
const nextItems = state.items.map(row =>
  row.sku === "B5" ? { ...row, qty: row.qty + 1 } : row
);
const next = { ...state, items: nextItems };
JavaScript

インデックス指定で更新(安全な境界ガード)

const i = 1;
const nextItems = state.items.map((row, idx) =>
  idx === i ? { ...row, qty: row.qty + 1 } : row
);
JavaScript

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

// 挿入
const insertAt = (arr, idx, item) => [
  ...arr.slice(0, idx), item, ...arr.slice(idx)
];

// 削除
const removeAt = (arr, idx) => [
  ...arr.slice(0, idx), ...arr.slice(idx + 1)
];

// 例
const items1 = insertAt(state.items, 1, { sku: "Z9", qty: 1 });
const items2 = removeAt(state.items, 0);
JavaScript

ここが重要です:配列に対する delete arr[i] は“穴(empty)”を作るため非推奨。必ず slicefiltersplice(破壊的なので慎重に) を使って詰め直します。


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

パス配列で更新する小さなユーティリティ

深い階層の更新は重複しやすいので、パス配列で指定できる汎用関数を用意すると保守性が上がります。

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

  if (rest.length === 0) {
    base[head] = value;
    return base;
  }
  base[head] = setPath(obj?.[head], rest, value);
  return base;
}

const next = setPath(state, ["user", "profile", "city"], "Osaka");
JavaScript

ここが重要です:この実装は“必要な階層だけ”を新しく作り直し、それ以外は元の参照を保つため効率的です。配列にも対応するよう、先頭で“配列ならコピー”の分岐を入れています。

値の更新ではなく“関数適用”で柔軟に

function updatePath(obj, path, fn) {
  const cur = path.reduce((acc, k) => acc?.[k], obj);
  const nextVal = fn(cur);
  return setPath(obj, path, nextVal);
}

// qty を +1
const next = updatePath(state, ["items", 1, "qty"], q => (q ?? 0) + 1);
JavaScript

欠損・既定値・安全性(重要ポイントの深掘り)

オプショナルチェーン+null合体で“落ちない更新”

読み取りに ?.、既定値に ?? を併用すると、欠損しても安全に意図を保てます。

// 読み取り(欠損なら 0)
const qty = state.items?.[1]?.qty ?? 0;

// 更新(受け皿を作りつつ)
const next = {
  ...state,
  items: (state.items ?? []).map((row, idx) =>
    idx === 1 ? { ...row, qty: (row.qty ?? 0) + 1 } : row
  )
};
JavaScript

浅いコピーの限界を理解する

{ ...obj }[...arr] は浅いコピーです。内側のオブジェクト・配列は参照共有のままなので、深い階層を更新したいときは“その階層でもう一段スプレッド”が必要です。全体を完全に独立させたいなら structuredClone を使います。

const deep = structuredClone(state); // 全階層を独立
JavaScript

パフォーマンスのバランス

巨大な構造を毎回全コピーするとコストが重くなります。“必要な階層だけ再構築”する設計(上の setPath/updatePath のような部分的コピー)を心がけましょう。


実践レシピ(よくある深い更新の定番)

設定オブジェクトの部分更新

function setTheme(state, theme) {
  return {
    ...state,
    user: {
      ...state.user,
      profile: { ...state.user.profile, theme }
    }
  };
}
JavaScript

明細行の検索と置換(複数条件)

function patchItem(state, pred, patch) {
  const items = (state.items ?? []).map(row =>
    pred(row) ? { ...row, ...patch } : row
  );
  return { ...state, items };
}

const next = patchItem(state, r => r.sku === "A1", { qty: 99 });
JavaScript

“なければ作る”更新(アップサート)

function upsertUserCity(state, city) {
  return {
    ...state,
    user: {
      ...(state.user ?? {}),
      profile: {
        ...(state.user?.profile ?? {}),
        city
      }
    }
  };
}
JavaScript

まとめ

深い階層の更新は「更新したい階層で必ずコピーし直す」ことが核心です。外→中→目的地の順でスプレッドし、配列は map/slice で非破壊に置換・挿入・削除。欠損には ?.?? を併用し、浅いコピーの限界を理解したうえで“必要な階層だけ再構築”する。汎用的にはパス指定のヘルパー(setPath/updatePath)を用意すれば、読みやすさと安全性、性能のバランスが取れます。これらを徹底すれば、初心者でも複雑なネスト構造を意図どおりに、短く堅牢に更新できます。

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