「浅いコピー」とは何か(まずイメージを掴む)
スプレッド構文のコピー([...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]
JavaScriptconst 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まとめ
スプレッド構文の「浅いコピー」の核心は、「外側の箱だけ新しくなり、中に入っているオブジェクトや配列は同じものを指し続ける」ということです。
配列やオブジェクトの「箱レベル」の変更(並び替え、一部プロパティの上書きなど)にはとても便利で、安全なコードを書きやすくしてくれます。
一方で、ネストした中身を「別物にしたつもり」で書き換えると、元のデータにも影響が出ます。
「浅いコピーで十分か?」「ネストも分けたいか?」を意識しつつ、基本は「一段目だけを変更する」「関数内では元を直接触らず浅いコピーから組み立てる」パターンを徹底すれば、初心者でもスプレッド構文のコピーを安心して使いこなせるようになります。
