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

JavaScript JavaScript
スポンサーリンク

何をしたいユーティリティか:「シャローコピー」

ここでの「シャローコピー」は、配列そのものだけをコピーして、“中身(要素)への参照はそのまま共有する”コピーのことです。
一言でいうと、「配列の箱だけ新しくして、中に入っているものはそのまま使い回すコピー」です。

業務では、次のような場面でよく使います。

  • 元の配列を壊さずに、並び替え・追加・削除だけしたい
  • React などで「新しい配列インスタンス」を作りたい(変更検知のため)
  • 大きなデータ構造を全部コピーするのは重いので、“箱だけ”差し替えたい

ディープコピーと違って「中身は共有」なので、どこまでが安全で、どこからが危険かを理解することがとても大事です。


まず前提:参照コピーとシャローコピーの違い

参照コピー(ただの代入)

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

ここでやっているのは「コピー」ではなく、同じ配列を指す変数を増やしているだけです。
ab は“別名”であって、中身は完全に同じ 1 つの配列です。

シャローコピー(箱だけ別)

const a = [1, 2, 3];
const b = a.slice(); // シャローコピー

b.push(4);

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

今度は、ab は「別の配列」です。
b に push しても a は変わりません。
これが「シャローコピー」の基本的なイメージです。


シャローコピーの実装:業務で使うならこの形

slice を使ったシャローコピー

一番オーソドックスな書き方です。

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

ポイントは次の2つです。

  • slice() を引数なしで呼ぶと、「全体のコピー」が作られる
  • Array.isArray で配列かどうかをチェックし、違ったら空配列を返す(業務で落ちないようにするため)

スプレッド構文を使ったシャローコピー

モダンな書き方はこちらです。

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

[...array] は、「array の中身を展開して、新しい配列に詰め直す」という意味です。
slice() と同じく、「箱だけ別、中身は同じ参照」のシャローコピーになります。


“シャロー”の意味:中身がオブジェクトのときに何が起きるか

ここが一番大事なポイントです。
シャローコピーは「配列の箱」は別ですが、「中のオブジェクト」は共有されます。

例:オブジェクト配列をシャローコピーした場合

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

b[0].id = 999;

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

なぜこうなるかというと、

  • ab は別の配列(箱は別)
  • しかし a[0]b[0] は同じオブジェクトを指している

からです。

つまり、「配列の構造(並び・要素の追加削除)を変える分には安全だが、中のオブジェクトを書き換えると元の配列にも影響する」ということです。


どんなときにシャローコピーで十分か

パターン1:並び替え・フィルタ・追加削除だけしたいとき

例えば、「選択中の ID リスト」を扱うケース。

const selectedIds = [1, 2, 3];

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

ここでは、中身はプリミティブ(数値)なので、シャローコピーで十分です。
また、「配列の構造(要素の追加)」だけを変えていて、中の値そのものは書き換えていません。

React などで「新しい配列インスタンスを作る」ことが目的なら、この使い方が典型です。

パターン2:API レスポンスの配列を“並びだけ”変えたいとき

function sortByCreatedAt(list) {
  const copied = shallowCopy(list);
  copied.sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt));
  return copied;
}
JavaScript

ここでは、「配列の順番だけ変えたい」のでシャローコピーで十分です。
中のオブジェクトのプロパティを書き換えていない限り、元データの“内容”は壊れません。


どんなときにシャローコピーでは危険か(ディープコピーが必要な場面)

パターン:中のオブジェクトも書き換えたいとき

例えば、「全ユーザーのフラグを一括で書き換える」ような処理。

function markAllActive(users) {
  const copied = shallowCopy(users);
  copied.forEach((u) => {
    u.active = true;
  });
  return copied;
}
JavaScript

一見よさそうに見えますが、これは元の users も書き換えています
なぜなら、u は元の配列の要素と同じオブジェクトだからです。

元データを壊したくないなら、ここはディープコピーが必要になります。

function deepCopy(value) {
  if (value === null || typeof value !== "object") {
    return value;
  }
  if (Array.isArray(value)) {
    return value.map((v) => deepCopy(v));
  }
  const result = {};
  for (const key in value) {
    if (Object.prototype.hasOwnProperty.call(value, key)) {
      result[key] = deepCopy(value[key]);
    }
  }
  return result;
}

function markAllActiveSafely(users) {
  const copied = deepCopy(users);
  copied.forEach((u) => {
    u.active = true;
  });
  return copied;
}
JavaScript

ここまでやって初めて、「元の users は一切変わらない」状態になります。


シャローコピー・ディープコピーの使い分けの感覚

ざっくりとした指針を言うと、こうです。

  • 配列の構造(順番・要素の追加削除)だけ変えたい
    → シャローコピーで十分(slice / スプレッド)
  • 中のオブジェクトのプロパティも書き換えたい
    → ディープコピーを検討する(少なくとも「元データを壊していいか」を意識する)

「とりあえず全部ディープコピー」は重くなりがちなので、
“何を変えたいのか”を意識して、シャローで足りるかどうかを判断するのがプロっぽい使い方です。


手を動かしてシャローコピーの感覚をつかむ

次のコードをコンソールで実行して、挙動を自分の目で確認してみてください。

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

const a1 = [1, 2, 3];
const b1 = shallowCopy(a1);
b1.push(4);
console.log("a1:", a1); // [1, 2, 3]
console.log("b1:", b1); // [1, 2, 3, 4]

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

前半では「箱だけ別になっている」こと、
後半では「中身のオブジェクトは共有されている」ことが、はっきり分かるはずです。


まとめ:シャローコピーは“箱だけ差し替える”ための道具

シャローコピーは、

  • 元の配列を壊さずに構造だけ変えたい
  • フレームワークの変更検知のために「新しい配列インスタンス」が欲しい

といった場面で、とてもよく効く“軽量なコピー”です。

プロジェクトに

export function shallowCopy(...) { ... }
JavaScript

のような関数を置き、「配列をいじる前に“とりあえず箱だけ別にしたい”ときは必ずこれを通す」と決めておくと、
「参照コピーの罠」にハマる回数がぐっと減ります。

そのうえで、「中身まで変えたいならディープコピーが必要だな」と意識できるようになると、
配列・オブジェクト周りのバグが一気に減って、コードの安定感が変わってきます。

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