JavaScript | 配列・オブジェクト:配列の変換・加工 – 非破壊的操作の考え方

JavaScript JavaScript
スポンサーリンク

非破壊的操作とは何か

非破壊的操作は「元の配列を変更せず、新しい配列を返す」考え方とテクニックの総称です。ここが重要です:配列を“直接書き換えない”ことで、予期せぬ影響(共有参照のバグ、UI状態の崩れ)を防げます。React/Vue などの状態管理、関数型スタイル、テスト容易性の向上に直結します。


破壊的操作と非破壊的操作の違い

代表的な破壊的操作(元配列が書き換わる)

// 破壊的
arr.sort();        // 並び替え
arr.reverse();     // 逆順
arr.splice(1, 2);  // 途中削除・挿入
arr.push(10);      // 末尾追加
arr.pop();         // 末尾削除
arr.shift();       // 先頭削除
arr.unshift(10);   // 先頭追加
JavaScript

破壊的操作は戻り値が“同じ配列の参照”であることが多く、他箇所が同じ配列を参照していると一斉に影響が及びます。

代表的な非破壊的操作(新しい配列を返す)

// 非破壊的
const b1 = arr.slice();       // 全体コピー
const b2 = arr.concat([10]);  // 連結
const b3 = arr.map(x => x*2); // 変換
const b4 = arr.filter(x => x>0); // 抽出
const b5 = arr.flat();        // 平坦化
const b6 = arr.toReversed?.(); // ES提案系:逆順の新配列(対応環境のみ)
const b7 = arr.toSorted?.();   // ES提案系:ソート済み新配列(対応環境のみ)
JavaScript

ここが重要です:標準で非破壊なもの(map/filter/flat など)を優先し、破壊的操作が必要なら“コピーしてから”使うのが安全です。


非破壊にする基本パターン(まずコピーする)

スプレッド・slice で浅いコピーを作る

const a = [1, 2, 3];

// 非破壊ソート
const sorted = [...a].sort((x, y) => x - y);

// 非破壊リバース
const reversed = a.slice().reverse();

console.log(a);        // [1, 2, 3](元はそのまま)
console.log(sorted);   // [1, 2, 3]
console.log(reversed); // [3, 2, 1]
JavaScript

ここが重要です:コピー→破壊的操作の順にすれば、外部に副作用は漏れません。浅いコピーなので“要素の参照”は同じ点に注意(オブジェクト要素の編集は別途対策)。

追加・削除・置換を非破壊で書く

const a = [1, 2, 3];

// 末尾に追加
const add = [...a, 4];           // 非破壊

// 先頭に追加
const unshifted = [0, ...a];

// 任意位置へ挿入
const inserted = [...a.slice(0, 1), 99, ...a.slice(1)]; // 1 の後ろに 99

// 条件削除(splice ではなく filter)
const removed = a.filter(x => x !== 2);

// 置換(map で差し替え)
const replaced = a.map(x => (x === 2 ? 200 : x));
JavaScript

ここが重要です:splice を避け、slice+スプレッド/filter/map を組み合わせるのが非破壊の基本レシピです。


参照の扱い(浅いコピーの限界と対策)

浅いコピーは“要素のオブジェクト参照”を共有する

const users = [{ id: 1, name: "A" }, { id: 2, name: "B" }];
const copy = [...users];   // 浅いコピー

copy[0].name = "A2";
console.log(users[0].name); // "A2"(同じ参照なので影響する)
JavaScript

ここが重要です:非破壊は“配列の並び・箱”を守るだけ。要素がオブジェクトなら参照は共有されます。編集時は“要素自体”を新規オブジェクトへ。

要素を安全に更新(スプレッドで新インスタンス)

const users = [{ id: 1, name: "A" }, { id: 2, name: "B" }];
const updated = users.map(u => (u.id === 2 ? { ...u, name: "B2" } : u));
// users は不変、updated は新しいオブジェクト参照
JavaScript

要素の差し替えはスプレッド({ …u, key: newValue })で“新インスタンス”を返すのが基本です。深いネストはライブラリや専用関数(構造的コピー)を検討します。


非破壊のメリット(実務で効くポイント)

状態管理が楽になる(予測可能性・差分検出)

非破壊は「前の配列と新しい配列が“別参照”」になるため、UIフレームワークが差分(=== の変化)を検出しやすく、再レンダリングが正しく行われます。バグ調査でも「いつ変更が起きたか」が辿りやすくなります。

テスト容易性(入力→出力が安定)

破壊的だと「グローバルな状態」が変わり、テストが相互依存しがちです。非破壊は“純粋な関数”に近づくため、テストと再利用が容易になります。

並行・共有の安全性

複数のモジュールやイベントが同じ配列を参照する状況で、非破壊なら“勝手に変わる”事故を防げます。ロックやディープコピーより軽く安全性を上げられます。


非破壊での加工レシピ(よくある実務パターン)

条件更新(ID マッチした要素だけ変更)

function updateById(arr, id, patch) {
  return arr.map(x => (x.id === id ? { ...x, ...patch } : x));
}
JavaScript

条件削除(1件だけ、複数件)

function removeById(arr, id) {
  return arr.filter(x => x.id !== id);
}
function removeByPredicate(arr, pred) {
  return arr.filter(x => !pred(x));
}
JavaScript

挿入(特定位置/条件位置の前に差し込む)

function insertAt(arr, index, value) {
  return [...arr.slice(0, index), value, ...arr.slice(index)];
}
function insertBefore(arr, pred, value) {
  const i = arr.findIndex(pred);
  return i >= 0 ? [...arr.slice(0, i), value, ...arr.slice(i)] : [...arr, value];
}
JavaScript

ソート・逆順(元を守りつつ)

const asc = [...arr].sort((a, b) => a - b);
const reversed = [...arr].reverse();
JavaScript

平坦化・絞り込み・変換の合成

const out = arr
  .filter(x => x.active)
  .flatMap(x => x.items)     // 1階層だけ展開
  .map(item => ({ ...item, flag: true }));
JavaScript

重要な注意点(深掘り)

疎配列(空スロット)への意識

forEach/map/filter などは“空スロットをスキップ”します。非破壊ロジックのはずが、穴が残って想定外になることがあります。必要なら先に正規化します。

const normalized = Array.from(arr, x => x ?? null); // 空スロットも値に
JavaScript

パフォーマンスとメモリ

非破壊は“新しい配列”を作るためメモリを使います。巨大配列で頻繁に再構築するなら、部分的な更新に絞る(slice の範囲を最小化)、データ構造を見直す(Map/Set、イミュータブルな木構造)などを検討します。

まとめて一度に(合成で1パスに)

filter → map → concat などを何段も重ねるとコストが増えます。必要に応じて reduce で“絞り込み+変換+集約”を1パスにまとめ、可読性と性能のバランスを取ります。

const labels = products.reduce((acc, p) => {
  if (p.stock > 0) acc.push(`${p.name} (${p.price})`);
  return acc;
}, []);
JavaScript

破壊的操作をどうしても使うなら

“コピーしてから”が鉄則です。さらに、破壊的操作直後は“元配列に依存する後続処理”と分離し、意図が明確な境界(関数やスコープ)を作ると事故が減ります。


実践例(UI状態・APIレスポンス・ログ)

UI選択のトグル(非破壊)

function toggle(arr, value) {
  return arr.includes(value)
    ? arr.filter(x => x !== value)
    : [...arr, value];
}
JavaScript

APIレスポンスのクレンジング(欠損除去+整形)

function cleanse(rows) {
  return rows
    .filter(x => x && typeof x.id === "number")
    .map(x => ({ id: x.id, name: String(x.name ?? "(unknown)") }));
}
JavaScript

ログ用配列の末尾追記(読み取り側を壊さない)

function appendLog(logs, entry) {
  return [...logs, { ...entry, ts: Date.now() }];
}
JavaScript

まとめ

非破壊的操作は「元配列を変えず、新しい配列で結果を返す」設計思想です。map/filter/flat/concat を基本に、破壊的操作が必要なら“コピーしてから”。オブジェクト要素は参照を共有するため、更新は“新インスタンス”で差し替える。疎配列の挙動、メモリと性能、合成での1パス最適化を意識すれば、初心者でも“意図通りで壊れにくい”配列加工を安定して書けます。

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