JavaScript | 配列・オブジェクト:パフォーマンス・設計 – 破壊的 / 非破壊的操作

JavaScript JavaScript
スポンサーリンク

破壊的 / 非破壊的操作とは何か

破壊的操作は「元の配列・オブジェクトを直接書き換える」こと、非破壊的操作は「元を保ったまま、新しい配列・オブジェクトを返す」ことです。ここが重要です:共有されているデータ(UI状態、キャッシュ、他モジュールが参照)に対して破壊的操作をすると予期せぬ副作用が起きがちです。反対に、非破壊は安全ですが“新インスタンスの作成コスト”が常に伴います。状況に応じて選択するのが設計の要です。

// 破壊的(元を変更)
const arr = [3,1,2];
arr.sort((a,b)=>a-b);  // arr が書き換わる

// 非破壊的(元を保つ)
const arr2 = [3,1,2];
const sorted = arr2.toSorted((a,b)=>a-b); // arr2 は維持、sorted は新規
JavaScript

代表的な破壊的 / 非破壊的操作

配列での例(どれが元を壊す?)

  • 破壊的: sort, reverse, splice, push, pop, shift, unshift
  • 非破壊的: toSorted, toReversed, toSpliced, slice, concat, map, filter, reduce, flat, flatMap
const a = [1,2,3];

// 破壊的
a.splice(1, 1);     // a => [1,3]
a.reverse();        // a => [3,1]

// 非破壊的
const b = a.slice(0, 2);      // 新しい配列、a はそのまま
const c = a.concat([4,5]);    // 新しい配列を返す
JavaScript

オブジェクトでの例(上書きとコピー)

  • 破壊的: 直接代入(obj.key = …)、delete、Object.assign(obj, …)
  • 非破壊的: スプレッド({ …obj, key: … })、Object.assign({}, obj, …)、構造化コピー(structuredClone)
const obj = { a: 1, b: 2 };

// 破壊的
obj.b = 3;
delete obj.a;

// 非破壊的(新インスタンスを返す)
const next = { ...obj, c: 4 };     // obj は維持
JavaScript

どちらを選ぶべきか(設計指針)

非破壊を優先する場面

UI状態管理(React/Vueなど)、履歴を持つ処理、複数モジュールで参照している共有データでは非破壊を基本にします。ここが重要です:非破壊にすると同一性(===)の変化で差分検知が安定し、バグ調査が容易になります。

// 状態の部分更新(非破壊)
const state = { user: { name: "A" }, prefs: { theme: "light" } };
const next = { 
  ...state, 
  prefs: { ...state.prefs, theme: "dark" } 
};
JavaScript

破壊が有効な場面

一時的でローカルなデータ、参照が他に渡らないことが自明、メモリや速度を極限まで詰めたいループ内部では破壊的が有利なことがあります。ここが重要です:外部に影響しない限定範囲でのみ使う、という線引きを徹底します。

// ローカルの一時配列を高速に並び替える(共有しない前提)
const tmp = getChunk();   // 新規生成
tmp.sort((a,b)=>a-b);     // 破壊的でも副作用は外へ漏れない
process(tmp);
JavaScript

パフォーマンスの現実(コストの比較)

破壊的の利点・欠点

破壊的操作は“新しい配列やオブジェクトを作らない”ので割り当てコストが小さく、ガベージコレクション(GC)の負荷も軽くなります。欠点は、副作用が外へ波及して“原因不明の挙動”を招くこと。とくに sort/reverse/splice は見た目の副作用が大きいので慎重に。

非破壊的の利点・欠点

非破壊操作は安全でテストしやすく、並行処理やUIの差分検知と相性が良い一方、毎回新インスタンスを割り当てるためメモリピークと割り当て時間が増えます。ここが重要です:大規模データでは“必要な部分だけコピー”に絞り、無駄な浅コピー(…)の連鎖を避けます。

// 悪い例:無駄な全体コピーの連続
let s = { a: { b: { c: 1 } } };
s = { ...s };                     // 意味がない全体コピー
s = { ...s, a: { ...s.a, b: { ...s.a.b, c: 2 } } };  // 必要階層だけで十分
JavaScript

事故を防ぐテクニック(重要ポイントの深掘り)

“入力を壊さない”ルールを関数の契約にする

関数は「引数を変更しない(非破壊)」を基本ポリシーにします。返り値で新値を渡す契約にすると、誰がいつ状態を変えたかが明確になります。

// 悪い:引数を書き換える
function addTagInPlace(item, tag) { item.tags.push(tag); return item; }

// 良い:新インスタンスで返す
function addTag(item, tag) { return { ...item, tags: [...(item.tags ?? []), tag] }; }
JavaScript

“書き換えても良い”ことを明示する

性能上、破壊的にしたいときは名前やコメントでスコープを明示します。レビューで見逃されにくくなります。

// 注意:破壊的に並び替え(ローカル専用)
function sortInPlace(nums) { nums.sort((a,b)=>a-b); return nums; }
JavaScript

ネスト更新は“階層ごとにコピー”

1段でもコピーを省くと参照共有が残り、他所から見えてしまいます。更新が必要な階層のみスプレッドで再構築するのが安全です。

const state = { list: [{ id: 1, active: false }] };
const next = {
  ...state,
  list: state.list.map(it => it.id === 1 ? { ...it, active: true } : it)
};
JavaScript

実務レシピ(安全・高速の両立)

非破壊ソート(toSorted)で事故を防ぐ

UI用の一覧は常に toSorted を使い、元配列は保つ方針にします。古い環境では slice().sort(...) で代替します。

const sorted = rows.toSorted((a,b)=>a.price-b.price);
// rows はそのまま
JavaScript

filter+map の1パス融合(非破壊・高効率)

中間配列を減らしつつ非破壊にするため、1回の走査で最終配列を作ります。

function filterMap(arr, pred, mapFn) {
  const out = [];
  for (const x of arr) { if (pred(x)) out.push(mapFn(x)); }
  return out;
}
JavaScript

大規模更新は“辞書化”して部分だけ置換

配列を丸ごと作り直すより、辞書で該当キーだけ非破壊更新し、表示時に配列へ戻すと効率的です。

const dict = Object.fromEntries(list.map(x => [x.id, x]));
const nextDict = { ...dict, [id]: { ...dict[id], active: true } };
const nextList = Object.values(nextDict);
JavaScript

まとめ

破壊的操作は速くメモリ効率も良い反面、共有状態で副作用を招きやすい。非破壊的操作は安全で差分検知に強いが、割り当てコストが増える。ここが重要です:UIや共有データでは“非破壊を原則”、ローカル・限定スコープでは“破壊も選択肢”。ソートや配列編集は非破壊版(toSorted / toReversed / toSpliced)を優先し、ネストは必要階層だけコピーする。この指針に従えば、初心者でも“壊さず速い”配列・オブジェクト操作を設計できます。

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