JavaScript Tips | 配列ユーティリティ:配列コピー

JavaScript JavaScript
スポンサーリンク

何をしたいユーティリティか:「配列コピー」

「配列コピー」は、元の配列を壊さずに、同じ中身を持つ“別の配列”を作る処理です。
業務ではほぼ必ずと言っていいほど使います。なぜなら、元の配列を直接いじると「いつの間にか順番が変わっている」「どこかで要素が消えている」といったバグの温床になるからです。

JavaScript では、配列は「参照型」です。
変数に入っているのは“配列そのもの”ではなく、“配列への参照”です。
なので、「コピーしたつもりで同じ配列を触っていた」という事故が起きやすいのです。

ここをきちんと理解して、「安全にコピーするユーティリティ」を持っておくと、一気にコードが安定します。


まず押さえるべき前提:参照とコピーの違い

参照をコピーしているだけのパターン

次のコードを見てください。

const a = [1, 2, 3];
const b = a;

b.push(4);

console.log(a); // [1, 2, 3, 4]
console.log(b); // [1, 2, 3, 4]
JavaScript

b = a; は「配列の中身をコピー」しているのではなく、「同じ配列を指す参照をコピー」しているだけです。
そのため、b.push(4)a にも影響します。

業務コードでこれをやると、「別の変数だと思っていたのに、片方を変えたらもう片方も変わる」という混乱が起きます。
これを避けるために、「ちゃんと別の配列を作る」=「配列コピー」が必要になります。


一番基本:浅いコピー(shallow copy)

slice を使った浅いコピー

配列を「浅くコピー」する一番基本の方法は slice() です。

function copyArray(array) {
  if (!Array.isArray(array)) {
    return [];
  }
  return array.slice();
}
JavaScript

ここでのポイントは、slice() を引数なしで呼ぶと「全体のコピー」が作られることです。
Array.isArray で配列かどうかをチェックし、違ったら空配列を返すようにしておくと、業務コードでも安全に使えます。

動きのイメージはこうです。

const a = [1, 2, 3];
const b = copyArray(a);

b.push(4);

console.log(a); // [1, 2, 3]
console.log(b); // [1, 2, 3, 4]
JavaScript

今度は b を変えても a は変わりません。
「別の配列」がちゃんとできている、ということです。

スプレッド構文を使った浅いコピー

同じことをスプレッド構文でも書けます。

function copyArraySpread(array) {
  if (!Array.isArray(array)) {
    return [];
  }
  return [...array];
}
JavaScript

[...array] は、「array の中身を展開して、新しい配列を作る」という意味です。
slice() と同じく、「浅いコピー」です。


浅いコピーの“浅い”とは何か

中身がオブジェクトの場合の挙動

浅いコピーは、「配列そのもの」は別になりますが、「中に入っているオブジェクト」は同じ参照のままです。

const a = [{ id: 1 }, { id: 2 }];
const b = copyArray(a);

b[0].id = 999;

console.log(a[0].id); // 999
console.log(b[0].id); // 999
JavaScript

ここで起きていることはこうです。

配列 a と配列 b は別物。
しかし、a[0] と b[0] は「同じオブジェクト」を指している。

つまり、「配列の箱」はコピーされているけれど、「箱の中身(オブジェクト)」は共有されている状態です。
これが「浅いコピー」です。

業務では、「配列の並びや要素の追加・削除だけを変えたい」なら浅いコピーで十分なことが多いです。
一方、「中のオブジェクトも含めて完全に独立させたい」なら、もう一段深いコピーが必要になります。


JSON で済むケース向け:簡易的な深いコピー

JSON で表現できるデータだけなら

「中身がプリミティブ・配列・プレーンオブジェクトだけ」で、
関数や Date、Map、Set などが含まれていない場合は、JSON.stringify / JSON.parse を使った簡易的な「深いコピー」が使えます。

function deepCopyJsonArray(array) {
  if (!Array.isArray(array)) {
    return [];
  }
  return JSON.parse(JSON.stringify(array));
}
JavaScript

これを使うと、配列の中のオブジェクトも含めて「別物」が作られます。

const a = [{ id: 1 }, { id: 2 }];
const b = deepCopyJsonArray(a);

b[0].id = 999;

console.log(a[0].id); // 1
console.log(b[0].id); // 999
JavaScript

今度は、b[0] を変えても a[0] は変わりません。
配列も中のオブジェクトも、完全に別のものになっています。

ただし、この方法には制限があります。
Date や Map、Set、関数などが含まれていると、正しくコピーできません。
「API レスポンスのような純粋な JSON データ」を扱うときにだけ使う、というルールにしておくと安全です。


業務での具体的な使い方

画面状態を更新するときに元配列を壊さない

例えば、選択中の ID リストを配列で持っているケース。

const selectedIds = [1, 2, 3];

function addSelection(id) {
  const next = copyArray(selectedIds);
  next.push(id);
  return next;
}
JavaScript

ここでのポイントは、「元の selectedIds を直接 push しない」ことです。
常に「コピーを作ってから変更する」ことで、「どこで何が変わったか」が追いやすくなります。

React などのフレームワークでも、「状態は不変(immutable)に扱う」ことが推奨されているので、
このパターンはそのまま役に立ちます。

API レスポンスを加工するときに元データを残す

API から配列が返ってきて、それを画面用に少し加工したいケース。

function prepareForView(rawList) {
  const list = copyArray(rawList);
  // 並び替えたり、フィルタしたりする
  return list;
}
JavaScript

rawList をそのままいじるのではなく、必ずコピーしてから加工することで、
「生データ」と「画面用データ」を分離できます。


配列コピーユーティリティで意識してほしいポイント

1. 「参照をコピーしているだけ」になっていないか常に疑う

const b = a; はコピーではなく「同じものを指しているだけ」です。
業務コードで配列を扱うときは、「これは本当に別配列か?」「同じ配列を触っていないか?」を常に意識してください。

そのために、「配列をいじる前にまず copyArray を通す」という癖をつけるのは、とても有効です。

2. 浅いコピーで十分な場面と、深いコピーが必要な場面を分ける

浅いコピー(slice / スプレッド)は、「配列の構造だけを変えたい」場面に向いています。
中のオブジェクトも含めて完全に独立させたいなら、JSON ベースの深いコピーや、もっと本格的な deep copy が必要です。

「とりあえず全部 deep copy」にすると、パフォーマンスが悪くなったり、Date などが壊れたりします。
なので、

配列の並び・要素の追加削除だけ → 浅いコピー
中身のオブジェクトも独立させたい → 深いコピー(対象データをよく確認)

という切り分けを意識してください。

3. ユーティリティで「配列じゃないもの」を受けても壊れないようにする

copyArraydeepCopyJsonArray は、必ず Array.isArray をチェックしています。

if (!Array.isArray(array)) {
  return [];
}
JavaScript

現実の業務コードでは、「本当は配列のはずだけど、何かの拍子に null やオブジェクトが来る」ことが普通にあります。
そこで例外を投げて落ちるより、「空配列を返す」と決めておくほうが、呼び出し側のコードがシンプルになります。


手を動かして感覚をつかむ

コンソールで、次のコードを実際に打ってみてください。

function copyArray(array) {
  if (!Array.isArray(array)) return [];
  return array.slice();
}

function deepCopyJsonArray(array) {
  if (!Array.isArray(array)) return [];
  return JSON.parse(JSON.stringify(array));
}

const a1 = [1, 2, 3];
const b1 = copyArray(a1);
b1.push(4);
console.log(a1, b1);

const a2 = [{ id: 1 }, { id: 2 }];
const b2 = copyArray(a2);
b2[0].id = 999;
console.log(a2[0].id, b2[0].id);

const a3 = [{ id: 1 }, { id: 2 }];
const b3 = deepCopyJsonArray(a3);
b3[0].id = 999;
console.log(a3[0].id, b3[0].id);
JavaScript

「どのケースで元の配列やオブジェクトが変わって、どのケースでは変わらないか」を、自分の目で確認してみてください。

そのうえで、自分のプロジェクトに

export function copyArray(...) { ... }
export function deepCopyJsonArray(...) { ... }
JavaScript

のような関数を置き、「配列をいじる前には必ず“配列コピーユーティリティ”を通す」と決めてみてください。
それだけで、「いつの間にかどこかで配列が書き換わっている」系のバグを、かなり減らせます。

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