JavaScriptの開発において、オブジェクトのコピーは日常的に行われる操作の一つです。
しかし、コピーの方法を誤ると、予期せぬバグやパフォーマンスの低下を招く原因となります。
特に「参照渡し」というJavaScript特有の性質を正しく理解していないと、元のデータを意図せず書き換えてしまう問題が発生します。
2026年現在のモダンな開発環境では、標準APIの進化により、かつては難しかったコピー操作も非常にシンプルかつ安全に行えるようになっています。
本記事では、用途に応じた最適なオブジェクトコピーの手法について、基礎から実践的なテクニックまで詳しく解説します。
オブジェクトのコピーにおける「参照」と「値」の違い
JavaScriptでオブジェクトをコピーする際にまず理解すべきなのは、変数に格納されているのはオブジェクトそのものではなく、メモリ上の場所を示す「参照」であるという点です。
単に変数から別の変数へ代入を行うと、実体は一つだけのまま、参照先だけが共有される状態になります。
// 参照の共有による問題の例
const user = { name: "田中" };
const copyUser = user; // 参照がコピーされる
copyUser.name = "佐藤";
console.log(user.name); // 元のオブジェクトも変更されてしまう
佐藤
このように、一方の値を変更するともう一方も影響を受ける状態は、多くのアプリケーション開発において不都合が生じます。
この問題を解決するために、元のオブジェクトとは別の実体をメモリ上に作成する「コピー」の操作が必要になります。
コピーには大きく分けて「シャローコピー(浅いコピー)」と「ディープコピー(深いコピー)」の2種類が存在します。
この違いを正しく理解することが、バグのない堅牢なコードを書くための第一歩となります。
シャローコピー(浅いコピー)とは
シャローコピーは、オブジェクトの「第一階層」のみを新しいメモリ領域にコピーする手法です。
オブジェクトが入れ子構造(ネスト)になっていない単純なデータ構造であれば、シャローコピーで十分対応可能です。
しかし、プロパティの値がさらに別のオブジェクトや配列である場合、そのネストされた要素については参照が引き継がれてしまいます。
そのため、階層の深い部分を書き換えると、やはり元のオブジェクトに影響を及ぼしてしまいます。
ディープコピー(深いコピー)とは
ディープコピーは、オブジェクトのすべての階層を再帰的にコピーし、完全に独立した新しいオブジェクトを作成する手法です。
ネストされたオブジェクトや配列も含めて、すべての値が新しいメモリ領域に複製されます。
複雑な状態管理を行う現代のフロントエンドフレームワークなどでは、このディープコピーが求められる場面が多々あります。
シャローコピーを実現する最適な手法
シャローコピーは処理が軽量であり、単純なデータの複製には非常に適しています。
現在、最も一般的に使われている2つの手法を紹介します。
1. スプレッド構文(…)を使用する
モダンなJavaScript開発において最も頻繁に利用されるのが、ES6から導入されたスプレッド構文です。
記述が簡潔で読みやすいため、多くの開発現場で標準的な手法として採用されています。
const original = { id: 1, name: "商品A" };
// スプレッド構文によるコピー
const copy = { ...original };
copy.name = "商品B";
console.log(original.name);
console.log(copy.name);
商品A
商品B
スプレッド構文は、新しいオブジェクトのリテラル内で既存のオブジェクトを展開するため、コピーと同時に新しいプロパティを追加したり、一部の値を上書きしたりする操作も容易に行えます。
ただし、前述の通りネストされた構造には対応できない点に注意してください。
2. Object.assign() メソッドを使用する
スプレッド構文が普及する前に主流だったのが、Object.assign()メソッドです。
ターゲットとなるオブジェクトに対して、一つ以上のソースオブジェクトから全ての列挙可能なプロパティをコピーします。
const target = { a: 1 };
const source = { b: 2, c: 3 };
const returnedTarget = Object.assign(target, source);
console.log(target);
{ a: 1, b: 2, c: 3 }
第一引数に空のオブジェクト{}を渡すことで、純粋なコピーを作成することができます。
スプレッド構文とほぼ同等の機能を持ちますが、Object.assign()はセッターを起動させるなどの細かな挙動の違いがあります。
現在の主流はスプレッド構文ですが、動的に複数のオブジェクトを合成する場合などはObject.assign()が便利な場面もあります。
ディープコピーを実現する最適な手法
オブジェクトが複雑にネストされている場合や、確実に元のデータとの繋がりを断ち切りたい場合には、ディープコピーを選択します。
1. structuredClone():2026年における標準的な選択肢
2026年現在、ディープコピーを行うための最も推奨される方法は structuredClone() 関数です。
これはブラウザやNode.jsに組み込まれた標準APIであり、高速かつ安全にディープコピーを実行します。
const complexObject = {
id: 101,
details: {
category: "家電",
tags: ["キッチン", "便利"]
},
date: new Date()
};
// 標準APIによるディープコピー
const deepCopy = structuredClone(complexObject);
deepCopy.details.category = "美容";
deepCopy.details.tags.push("新製品");
console.log("Original:", complexObject.details.category);
console.log("Original Tags:", complexObject.details.tags);
console.log("Copy Tags:", deepCopy.details.tags);
Original: 家電
Original Tags: ["キッチン", "便利"]
Copy Tags: ["キッチン", "便利", "新製品"]
structuredClone() の優れた点は、DateオブジェクトやMap、Set、さらにはArrayBufferといった特殊な型も正しく複製できることです。
また、循環参照(自分自身を参照しているオブジェクト)が含まれていてもエラーにならずに処理できる強力な特徴を持っています。
外部ライブラリを導入することなく、ネイティブの機能だけで完結できるため、最優先で検討すべき手法と言えるでしょう。
2. JSON.parse() と JSON.stringify():古くからの代替案
structuredClone() が普及するまで長年使われてきたのが、JSON関数を組み合わせる手法です。
一度オブジェクトを文字列に変換し、それを再びオブジェクトに戻すことで、結果的に参照を完全に切り離します。
const obj = { a: 1, b: { c: 2 } };
const jsonCopy = JSON.parse(JSON.stringify(obj));
手軽に実装できる一方で、この手法には重大な制限事項がいくつか存在します。
まず、関数(Function)やundefined、SymbolなどはJSON形式で表現できないため、コピーの過程で消失してしまいます。
また、Dateオブジェクトは文字列に変換されたまま元に戻らず、NaNやInfinityはnullに変換されてしまいます。
構造が単純なプレーンなオブジェクト(POJO)に限定して使用するのであれば問題ありませんが、現代の開発では避けた方が無難です。
3. 外部ライブラリ(LodashのcloneDeepなど)
プロジェクトですでに Lodash などのユーティリティライブラリを使用している場合は、その機能を利用することもあります。
Lodashの cloneDeep は非常に高機能で、古くから信頼性の高いディープコピー手段として重宝されてきました。
しかし、現代では structuredClone() で事足りることが多いため、コピーのためだけに巨大なライブラリを新規導入するのは推奨されません。
各コピー手法の比較表
状況に応じて適切な手法を選択できるよう、各手法の特徴を一覧表にまとめました。
| 手法 | コピーの種類 | 特殊な型(Dateなど) | 循環参照 | パフォーマンス |
|---|---|---|---|---|
| スプレッド構文 | シャロー | △(参照維持) | × | 非常に高速 |
| Object.assign | シャロー | △(参照維持) | × | 高速 |
| structuredClone | ディープ | ○(対応) | ○(対応) | 標準的 |
| JSON.parse/stringify | ディープ | ×(消失または変質) | ×(エラー) | 低速 |
実際の開発現場での使い分け基準
どの手法を使うべきかは、扱うデータの構造と、アプリケーションが求める要件によって決まります。
ReactやVueなどの状態管理(State)の場合
モダンなフロントエンド開発では、「イミュータビリティ(不変性)」が極めて重要です。
Reactの useState などでオブジェクトを更新する場合、一般的にはスプレッド構文が多用されます。
これは、状態全体をディープコピーするよりも、必要な階層だけを新しく生成する方がパフォーマンス面で有利だからです。
// Reactでの典型的な更新パターン
const updateUserName = (newName) => {
setUser(prevUser => ({
...prevUser,
name: newName
}));
};
このように、浅い階層の更新であればスプレッド構文による記述が最適解となります。
APIからのレスポンスを加工する場合
バックエンドから取得した複雑なJSONデータを加工しつつ、元のデータも保持しておきたい場合は、structuredClone() が適しています。
特に設定値のデフォルトオブジェクトを保持したまま、ユーザーの入力に応じて一部を深く書き換えるようなケースでは、ディープコピーが必須です。
「元のデータを汚染しない」という安全性を最優先にするなら、迷わず structuredClone() を選択するのが2026年現在のベストプラクティスです。
コピー時の注意点と落とし穴
オブジェクトのコピーを行う際には、いくつか注意すべき特殊なケースがあります。
プロトタイプチェーンの継承
スプレッド構文や structuredClone() は、オブジェクトの「自分自身のプロパティ(own properties)」のみをコピーします。
クラスのインスタンスをコピーしようとしても、継承されたメソッドやプロトタイプ情報は引き継がれません。
インスタンスの完全な複製が必要な場合は、そのクラス自体に clone() メソッドなどを実装し、手動で新しいインスタンスを生成する必要があります。
関数のコピー
基本的に、関数を「コピー」することはできません。
structuredClone() は関数が含まれているオブジェクトを渡すとエラーを投げます。
これは、関数が現在の実行コンテキストやクロージャに依存しているため、単純な値として複製することが論理的に難しいためです。
オブジェクトの中にメソッドが含まれる場合は、シャローコピーで参照を維持するか、あるいは設計自体を見直してデータと関数を分離することを検討してください。
パフォーマンスに関する考察
コピー操作は、メモリの確保とデータの走査を伴うため、コストの低い処理ではありません。
非常に大きな配列や、数千のプロパティを持つ巨大なオブジェクトを頻繁にコピーすると、画面のカクつきやレスポンスの低下を招きます。
特にアニメーションループ内や、高頻度で発生するイベントリスナー内でのディープコピーは避けるべきです。
パフォーマンスがボトルネックになる場合は、「本当にコピーが必要か?」を問い直し、必要な差分だけを管理するデータ構造への変更を検討しましょう。
まとめ
JavaScriptにおけるオブジェクトのコピーは、一見単純に見えて非常に奥が深いテーマです。
2026年のモダンな開発においては、以下の3つのポイントを基準に手法を選択しましょう。
- 第一階層のみの単純なコピーやReact等の状態更新:簡潔で高速な「スプレッド構文」を使用する。
- ネストされた複雑なオブジェクトの完全な複製:標準APIである「
structuredClone()」を使用する。 - JSON手法や外部ライブラリ:特定の制約がある場合を除き、積極的な使用は控える。
JavaScriptの参照の仕組みを正しく理解し、適切なコピー手法を使い分けることで、意図しないバグを防ぎ、メンテナンス性の高いコードを実現できます。
まずはデータの構造を確認し、シャローコピーで十分なのか、それともディープコピーが必要なのかを判断する習慣をつけましょう。
この記事で紹介した手法を活用し、より堅牢なJavaScriptアプリケーションの開発に役立ててください。
