JavaScript | ES6+ 文法:スプレッド構文 – 浅いコピーの理解

JavaScript
スポンサーリンク

「浅いコピー」とは何か(まずイメージを掴む)

スプレッド構文のコピー([...arr]{ ...obj })は「浅いコピー(shallow copy)」です。
ここが最重要ポイントです:

  • 外側の「箱」(配列そのもの・オブジェクトそのもの)は新しく作られる
  • しかし「中に入っているオブジェクトや配列」は、新しくならず「同じもの(参照)」を指す

つまり、「一段目だけコピーして、二段目以降は共有している」状態になります。

const obj = { a: 1, b: 2 };
const copy = { ...obj };

console.log(copy);        // { a: 1, b: 2 }
console.log(copy === obj); // false(外側は別物)
JavaScript

配列での浅いコピー(中身がオブジェクトのときに注意)

数値や文字列だけの配列の場合

要素がプリミティブ(数値・文字列・真偽値など)だけなら、あまり深く意識しなくても大丈夫です。

const arr = [1, 2, 3];
const copy = [...arr];

copy[0] = 99;

console.log(arr);  // [1, 2, 3](元はそのまま)
console.log(copy); // [99, 2, 3]
JavaScript

ここでは「箱(配列)も中身(値)も別々」に見えるので、「浅いコピー」だと意識しにくいかもしれません。
問題は「中身がオブジェクトのとき」です。

要素がオブジェクトの配列の場合

配列自体は別ですが、中のオブジェクトは同じものを指します。

const users = [
  { name: "Alice" },
  { name: "Bob" }
];

const copy = [...users];

copy[0].name = "Charlie";

console.log(users[0].name); // "Charlie"
console.log(copy[0].name);  // "Charlie"
console.log(users === copy);        // false(配列は別)
console.log(users[0] === copy[0]);  // true(中身のオブジェクトは同じ)
JavaScript

ここが重要です:

  • [...users] は「配列という入れ物」をコピーしているだけ
  • 入れ物の中身(オブジェクト)は同じものを指している

「浅いコピー」の正体は、この「一段目だけ別で、二段目以降は共有」という状態です。

オブジェクトでの浅いコピー(ネストしたプロパティが落とし穴)

一段目だけ見ていると「完全コピー」に見える

const state = {
  count: 0,
  flag: true
};

const copy = { ...state };

copy.count = 1;

console.log(state.count); // 0
console.log(copy.count);  // 1
console.log(state === copy); // false
JavaScript

ここだけ見ると、「きれいにコピーされてるじゃん?」と感じます。
しかし、プロパティの中にオブジェクトが入ってくると話が変わります。

プロパティの中にオブジェクトがある場合

const state = {
  count: 0,
  user: { name: "Alice", age: 20 }
};

const copy = { ...state };

// ネストした user の中身を変える
copy.user.name = "Bob";

console.log(state.user.name); // "Bob"
console.log(copy.user.name);  // "Bob"
console.log(state === copy);            // false(外側は別)
console.log(state.user === copy.user);  // true(内側は同じ)
JavaScript

ここが重要です:

  • { ...state } は「state の各プロパティへの“リンク”をコピー」しているイメージ
  • user 自体は同じオブジェクトなので、中身を書き換えると元にも反映される

浅いコピーを使うとき、「ネストした中身まで独立させたいのか」「それとも共有でいいのか」を意識する必要があります。

「浅いコピー」で何が嬉しくて、どこが危ないのか

嬉しいポイント(よくある用途)

浅いコピーでも、次のような場面では十分役に立ちます。

  • 配列の並び替え([...arr].sort(...))で元を壊さない
  • 設定オブジェクトの「一部だけ上書きした新バージョン」を作る
  • 元を残したまま、「箱(配列/オブジェクト)」レベルの違いだけ表現したい
const scores = [72, 88, 95, 64];
const sorted = [...scores].sort((a, b) => a - b);

console.log(scores); // [72, 88, 95, 64]
console.log(sorted); // [64, 72, 88, 95]
JavaScript
const config = { theme: "light", lang: "ja", debug: false };
const newConfig = { ...config, debug: true };

console.log(config);    // debug: false
console.log(newConfig); // debug: true
JavaScript

危ないポイント(バグの元になりやすいところ)

危険なのは、「中身までコピーされたと思い込んで、ネストしたオブジェクトをいじってしまう」ことです。

const state = {
  user: { name: "Alice" }
};

const copy = { ...state };

// 「copy 側だけ user.name を変えるつもり」で書いてしまう
copy.user.name = "Bob";

console.log(state.user.name); // "Bob"(元も変わってしまう)
JavaScript

ここが重要です:

  • 浅いコピーは「箱を分けただけで、中のモノは同じ」
  • ネストした中身を変えたら、元にも影響する

「変えていいのは“箱”だけなのか?」「中身も完全に分離したいのか?」を意識して設計するのが大事です。

浅いコピーで十分なときと、ディープコピーが必要なとき

浅いコピーで十分な典型パターン

次のような状況では、浅いコピーで問題ないことが多いです。

  • 配列の要素がプリミティブ(数値・文字列・真偽値など)だけ
  • ネストしたオブジェクトを「読み取り専用」として扱う(中身を書き換えない)
  • 外側の構造だけ変えたい(例:count を増やす、debug フラグを切り替える)
const state = {
  count: 0,
  user: { name: "Alice" } // 中身は読み取り専用と決めている
};

const next = { ...state, count: state.count + 1 };

console.log(state.count); // 0
console.log(next.count);  // 1
// user は共有だが、「user を変えない」という前提なら問題にならない
JavaScript

ディープコピーを考えたほうがいい状況(概要だけ)

初心者のうちは、まず「そんな場面がある」という感覚だけで十分ですが、例えば次のようなときはディープコピーが検討対象になります。

  • ネストしたオブジェクトや配列を「新しいものとして編集」したい
  • テストなどで「元のデータと完全に独立したコピー」が必要
  • 外部ライブラリから渡されたオブジェクトを、安全に独立させて扱いたい

ディープコピーは、
JSON シリアライズ/専用ライブラリ/再帰的コピーなどいろいろ方法がありますが、
まずは「浅いコピーではネストが共有される」という事実を理解するのが先です。

浅いコピーを前提にした“安全な使い方”パターン

ルール1:書き換えるのは基本「一段目」だけにする

浅いコピーを使うときは、「書き換えていいのは一段目だけ」にすると安全です。

const state = {
  user: { name: "Alice", age: 20 },
  ui: { theme: "light" }
};

// count など一段目だけ変えるイメージで使う
const next = {
  ...state,
  ui: { ...state.ui, theme: "dark" } // ネストも“浅いコピー+上書き”の組み合わせで対応
};
JavaScript

ネストも変えたいなら、そこも改めてスプレッドで「浅いコピー」を取り直す
ui: { ...state.ui, theme: "dark" } のように)というパターンが使えます。

ルール2:関数内では「元を直接触らない」

外部から渡されたオブジェクトや配列は直接書き換えず、
浅いコピーを作ってから「箱レベル」で差分を作るようにすると安全です。

function enableDebug(config) {
  // config は触らず、新しいオブジェクトを返す
  return { ...config, debug: true };
}

const base = { debug: false };
const updated = enableDebug(base);

console.log(base.debug);    // false
console.log(updated.debug); // true
JavaScript

ルール3:ネストを書き換えるときは「意識的に」

ネストしたオブジェクトの中身を書き換えるときは、
「元にも影響する」ことを理解した上で意図的に行うか、
もしくは「そこもコピーを取りたいのか?」を一度考えてみましょう。

const state = { user: { name: "Alice" } };
const copy = { ...state };

// 本当に state.user も変わっていいならこれでOK
copy.user.name = "Bob";

// もし分けたいなら、user もコピーする
const safeCopy = {
  ...state,
  user: { ...state.user }
};
safeCopy.user.name = "Carol";

console.log(state.user.name);   // "Bob"
console.log(safeCopy.user.name); // "Carol"
JavaScript

例題で理解を固める

// 1) 配列浅いコピー(プリミティブ)
const nums = [1, 2, 3];
const numsCopy = [...nums];
numsCopy[0] = 99;
console.log(nums);     // [1, 2, 3]
console.log(numsCopy); // [99, 2, 3]

// 2) 配列浅いコピー(中身がオブジェクト)
const list = [{ id: 1 }, { id: 2 }];
const listCopy = [...list];
listCopy[0].id = 999;
console.log(list[0].id);     // 999(共有)
console.log(listCopy[0].id); // 999

// 3) オブジェクト浅いコピー(ネストなし)
const cfg = { theme: "light", lang: "ja" };
const cfgCopy = { ...cfg };
cfgCopy.theme = "dark";
console.log(cfg.theme);     // "light"
console.log(cfgCopy.theme); // "dark"

// 4) オブジェクト浅いコピー(ネストあり)
const state = { ui: { theme: "light" } };
const stateCopy = { ...state };
stateCopy.ui.theme = "dark";
console.log(state.ui.theme);     // "dark"(共有)
console.log(stateCopy.ui.theme); // "dark"

// 5) ネストもコピーして分離したい場合のパターン
const original = { ui: { theme: "light", size: "m" } };
const deepish = {
  ...original,
  ui: { ...original.ui, theme: "dark" }
};
console.log(original.ui.theme); // "light"
console.log(deepish.ui.theme);  // "dark"
JavaScript

まとめ

スプレッド構文の「浅いコピー」の核心は、「外側の箱だけ新しくなり、中に入っているオブジェクトや配列は同じものを指し続ける」ということです。
配列やオブジェクトの「箱レベル」の変更(並び替え、一部プロパティの上書きなど)にはとても便利で、安全なコードを書きやすくしてくれます。

一方で、ネストした中身を「別物にしたつもり」で書き換えると、元のデータにも影響が出ます。
「浅いコピーで十分か?」「ネストも分けたいか?」を意識しつつ、基本は「一段目だけを変更する」「関数内では元を直接触らず浅いコピーから組み立てる」パターンを徹底すれば、初心者でもスプレッド構文のコピーを安心して使いこなせるようになります。

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