オブジェクトのコピーとは何か
「コピー」は、元のオブジェクトから同じ内容を持つ“別のオブジェクト”を作ることです。ここが重要です:JavaScriptのオブジェクトは参照型なので、単なる代入は「同じものを指すようになるだけ」でコピーではありません。独立したオブジェクトが必要なら、意識的に「複製」を作る必要があります。
const original = { n: 1 };
const alias = original; // 参照を共有(コピーではない)
alias.n = 99;
console.log(original.n); // 99(同じものを見ている)
JavaScript浅いコピー(shallow copy)とは何か
浅いコピーは「一段目だけ複製」します。入れ子のオブジェクトや配列は“参照のまま”コピーされるため、深い部分は共有されます。
const original = { n: 1, nested: { x: 10 } };
// スプレッド構文
const copy1 = { ...original };
// Object.assign
const copy2 = Object.assign({}, original);
copy1.nested.x = 99;
console.log(original.nested.x); // 99(内側は共有されている)
JavaScriptここが重要です:浅いコピーは「表面のプロパティだけ別物」にしたいときに十分ですが、ネストを変更すると元にも影響します。入れ子も独立させたいなら「深いコピー」が必要です。
深いコピー(deep copy)の基本
深いコピーは「入れ子を含めて丸ごと複製」します。モダンな方法は structuredClone がシンプルで安全です。
const original = { n: 1, nested: { x: 10 }, arr: [1, 2, 3] };
const deep = structuredClone(original);
deep.nested.x = 99;
deep.arr.push(4);
console.log(original.nested.x); // 10(独立)
console.log(original.arr); // [1, 2, 3](独立)
JavaScriptここが重要です:structuredClone はオブジェクト、配列、Map/Set、Date、TypedArray など広く対応します。関数やプロトタイプ、DOMノードなどクローン不可のものは対応外です。
JSON を使った簡易的な深いコピー(制約を理解する)
JSON.stringify + JSON.parse は手軽ですが、制約が多い「擬似的な深いコピー」です。
const original = { n: 1, nested: { x: 10 }, date: new Date(), fn: () => {} };
const deep = JSON.parse(JSON.stringify(original));
console.log(deep); // { n: 1, nested: { x: 10 } }(dateや関数は失われる)
JavaScriptここが重要です:JSONは「数値・文字列・真偽値・null・配列・プレーンオブジェクト」しか表現できません。関数、undefined、Symbol、Date、Map/Setなどは消えたり文字列化されます。データだけの構造ならOK、リッチな型を含むなら不適です。
配列のコピーと「配列の中のオブジェクト」
配列自体のコピーは浅いコピーが基本です。中の要素がオブジェクトなら、それらは参照のままです。
const arr = [{ n: 1 }, { n: 2 }];
// 浅いコピー(配列本体は別、要素は同じ参照)
const arrCopy = [...arr];
arrCopy[0].n = 99;
console.log(arr[0].n); // 99(要素は共有)
// 要素ごとも独立させるには要素単位でコピー
const deepArr = arr.map(item => structuredClone(item));
deepArr[0].n = 123;
console.log(arr[0].n); // 99(独立)
JavaScriptここが重要です:配列の「本体をコピー」するのと「要素の中身までコピー」するのは別問題です。必要に応じて要素単位のコピーも行いましょう。
プロパティ属性・プロトタイプの扱い(一歩深掘り)
- 省略的な浅いコピー(スプレッド・assign)は「列挙可能な自分のプロパティ」を複製します。getter/setterは「結果の値」が複製され、アクセス器そのものは維持されません。
- プロトタイプや非列挙プロパティ、プロパティ属性(writable/enumerable/configurable)まで保ちたい場合は、プロパティディスクリプタを使います。
const source = {};
Object.defineProperty(source, "hidden", {
value: 1, enumerable: false, writable: false, configurable: false
});
const shallow = { ...source };
console.log(Object.keys(shallow)); // [](非列挙はコピーされない)
// ディスクリプタごと複製(例:ライブラリを使うか自前で)
const descriptors = Object.getOwnPropertyDescriptors(source);
const exact = Object.create(Object.getPrototypeOf(source));
Object.defineProperties(exact, descriptors);
JavaScriptここが重要です:通常のアプリでは「列挙可能な自分のプロパティだけ」で十分ですが、ライブラリやフレームワークの内部では属性や原型の保持が必要になることがあります。
変更を安全に扱う設計(イミュータブル思考のコツ)
コピーは「元を壊さないため」にあります。更新は「コピーを作って変更」する流れにすると安全です。
// オブジェクトを“差分更新”で新しくする
const user = { name: "太郎", profile: { city: "東京" } };
// 浅い更新(profileごと置き換えるならOK)
const updated1 = { ...user, name: "花子" };
// 深い部分だけ更新(内側もコピーしてから)
const updated2 = {
...user,
profile: { ...user.profile, city: "大阪" }
};
console.log(user.profile.city); // 東京(元は不変)
console.log(updated2.profile.city); // 大阪(新しい)
JavaScriptここが重要です:ReactなどのUIフレームワークでは「新しいオブジェクトを返す」ことが差分検知の鍵になります。浅い・深いの線引きを意識して、必要な層だけコピーするのが効率的です。
まとめ
- 代入はコピーではなく「参照の共有」。独立させたいならコピーを作る。
- 浅いコピーは「一段目のみ複製」、ネストは共有される(スプレッド、Object.assign)。
- 深いコピーは「入れ子まで複製」、
structuredCloneが簡単で強力。JSONは制約が多い。 - 配列本体のコピーと、要素(オブジェクト)のコピーは別物。必要なら要素単位で複製。
- プロパティ属性やプロトタイプまで必要ならディスクリプタで扱う。
- 更新は「コピーしてから変更」するイミュータブル思考で、予測可能で壊れにくいコードになる。
まずは「浅いコピー」と「深いコピー」の違いを体に覚えさせ、... と structuredClone を使い分けて小さな練習を重ねるのが近道です。
