はじめに
こんにちは、株式会社iimonでフロントエンドエンジニアをしているnkmです!
本記事はiimonアドベントカレンダー2日目の記事となります。
今回は、以下について実践的な例を交えながら解説していきます!
・シャローコピーとディープコピーの基礎
・シャローコピーとディープコピーの違い
・シャローコピーの3つの方法、使い分け
・ディープコピーの4つの方法、使い分け
シャローコピーとディープコピーの基礎
オブジェクトの参照の仕組み
JavaScriptでは、オブジェクトは参照によって管理されています。そのうちプリミティブ値と呼ばれるものと、参照値と呼ばれるものがあり、データの取り扱い方が異なります。この性質は、シャローコピーとディープコピーの違いを理解する上で重要です。
プリミティブ値(基本型)
これらは値そのものが変数に格納され、代入時には値のコピーが行われます。
・文字列(String)
・数値(Number)
・真偽値(Boolean)
・null
・undefined
・Symbol
・BigInt
参照値(参照型)
これらは値への参照(メモリ上のアドレス)が変数に格納され、代入時には参照のコピーが行われます。
・オブジェクト(Object)
・配列(Array)
・関数(Function)
・Date
・RegExp など
// プリミティブ値の場合 let a = 10; let b = a; // 値のコピー a = 20; console.log(b); // 10 (aの変更はbに影響しない) // 参照値の場合 let obj1 = { x: 10 }; let obj2 = obj1; // 参照のコピー obj1.x = 20; console.log(obj2.x); // 20 (obj1の変更がobj2に影響する)
シャローコピーとディープコピーの違い
const original = { primitive: 100, // プリミティブ値 object: { value: 200 } // 参照値 }; // シャローコピー const shallow = { ...original }; original.primitive = 150; // プリミティブ値の変更 original.object.value = 250; // 参照先の値の変更 console.log(shallow.primitive); // 100 (影響を受けない) console.log(shallow.object.value); // 250 (影響を受ける) // ディープコピー const deep = JSON.parse(JSON.stringify(original)); original.object.value = 300; console.log(deep.object.value); // 250 (影響を受けない)
以上のコードからシャローコピーとディープコピーの以下の違いがわかります。
シャローコピー
プリミティブ値 → 新しい値として完全コピー
参照値 → 参照のみコピー(同じメモリ位置を指す)
ディープコピー
プリミティブ値 → 新しい値として完全コピー
参照値 → 新しいメモリ位置に完全コピー(参照、値も新しく作成)
シャローコピーの3つの方法
// 元のオブジェクト const user = { name: "山田太郎", age: 30, profile: { job: "エンジニア" } }; // 1. スプレッド構文 const copy1 = { ...user }; // 2. Object.assign() const copy2 = Object.assign({}, user); // 3. Array.from()(配列の場合) const arrayCopy = Array.from(user.profile.skills);
それぞれの特徴
1. スプレット構文
const original = { name: "田中", age: 30, hobbies: ["読書", "散歩"] }; const copy = { ...original }; // オブジェクトの場合 const updatedUser = { ...user, age: 31 }; // プロパティの追加や上書きが簡単 // 配列の場合 const newArray = [...array, newItem]; // 配列の結合も簡単
特徴:
✅ 最も簡潔で読みやすい構文
✅ オブジェクトと配列の両方に使える
✅ 新しいプロパティの追加が容易
❌ ES6以降でのみ使用可能
2. Object.assign()
const original = { name: "田中", age: 30, hobbies: ["読書", "散歩"] }; const copy = Object.assign({}, original); // 複数オブジェクトのマージ const merged = Object.assign({}, obj1, obj2, obj3); // 既存オブジェクトへの追加 Object.assign(existingObj, newProperties);
特徴:
✅ ES5からサポート(古いブラウザでも動作)
✅ 複数のオブジェクトをマージ可能
✅ 第一引数に既存のオブジェクトを指定可能
❌ 構文がやや冗長
❌ 配列には適していない
なぜ配列に適さないのか(備考)
Object.assign()が配列に適さない理由
const array = ['a', 'b', 'c']; const copyWithAssign = Object.assign([], array); console.log(copyWithAssign); // ['a', 'b', 'c']
一見うまく動作しているように見えますが、以下のような問題があります
疎配列(sparse array)の扱い
const sparseArray = ['a', ,'c']; // インデックス1は空 console.log(sparseArray.length); // 3 // Object.assign()でコピー const copyWithAssign = Object.assign([], sparseArray); console.log(copyWithAssign); // ['a', undefined, 'c'] console.log(1 in copyWithAssign); // true (望ましくない) // Array.from()でコピー const copyWithArrayFrom = Array.from(sparseArray); console.log(copyWithArrayFrom); // ['a', undefined, 'c'] console.log(1 in copyWithArrayFrom); // false (望ましい) // スプレッド構文でコピー const copyWithSpread = [...sparseArray]; console.log(copyWithSpread); // ['a', undefined, 'c'] console.log(1 in copyWithSpread); // false (望ましい)
配列メソッドの継承問題
スプレット構文とは追加されたプロパティの扱いが異なります
const array = ['a', 'b', 'c']; // 配列に独自のメソッドを追加 array.customMethod = function() { return 'test'; }; // Object.assign()でコピー const copyWithAssign = Object.assign([], array); console.log(copyWithAssign.length); // 3 console.log(copyWithAssign.customMethod); // function // スプレッド構文でコピー const copyWithSpread = [...array]; console.log(copyWithSpread.length); // 3 console.log(copyWithSpread.customMethod); // undefined
✅ 配列の要素をコピー
✅ 列挙可能なプロパティもコピー
👉 元の配列のプロパティをそのまま維持したい場合に有用
3. Array.from()
const original = ["読書", "散歩", "料理"]; const copy = Array.from(original); // イテラブルからの配列作成 const arrayFromSet = Array.from(new Set([1, 2, 3])); // マッピング関数の使用 const mapped = Array.from([1, 2, 3], x => x * 2);
特徴:
✅ 配列に特化したメソッド
✅ イテラブルオブジェクトを配列に変換可能
✅ マッピング関数を使用可能
❌ オブジェクトには使用不可
❌ 配列のみに限定
使い分けの指針
モダンなJavaScriptを使用する場合
オブジェクト:スプレッド構文を使用
配列:スプレッド構文を使用
古いブラウザのサポートが必要な場合
オブジェクト:Object.assign()を使用
配列:Array.from()を使用
特殊なケース
イテラブルオブジェクトを配列に変換:Array.from()
複数オブジェクトのマージ:Object.assign()または複数のスプレッド構文
ディープコピー
ディープコピーは、上記であげたように以下の特徴があります。
プリミティブ値 → 新しい値として完全コピー
参照値 → 新しいメモリ位置に完全コピー(参照、値も新しく作成)
ディープコピーはオブジェクトの保持する値も複製して、別の独立したオブジェクトを新たに作成するということです。
ディープコピーの4つの方法
1. structuredClone() structuredCloneは2022年以降推奨されるディープコピーの標準的な方法です。
const original = { name: "山田", age: 30, address: { city: "東京", district: "渋谷区" }, hobbies: ["読書", "旅行"], birthday: new Date('1990-01-01'), scores: new Map([['math', 90], ['english', 85]]), tags: new Set(['student', 'developer']) }; const cloned = structuredClone(original);
それ以前に標準的だったJSON.parse(JSON.stringify())
と比較すると、以下のような点で優れていると言えます。
特徴:
✅ 循環参照を正しく処理
❌ 関数はエラー(意図的な制限)
✅ undefined を保持
✅ Date オブジェクトを正しくコピー
✅ Map と Set を正しくコピー
✅ RegExp を正しくコピー
2. JSON.parse(JSON.stringify())
const original = { name: "山田", age: 30, address: { city: "東京" } }; const cloned = JSON.parse(JSON.stringify(original)); // 注意が必要なケース const problematicObject = { date: new Date(), undefined: undefined, function: () => console.log("hello"), symbol: Symbol("test"), regexp: /test/, }; const cloned = JSON.parse(JSON.stringify(problematicObject)); console.log(cloned); // { // date: "2024-12-01T07:26:00.000Z", // 文字列に変換 // regexp: {}, // 空オブジェクトに // // undefined, function, symbol は完全に失われる // }
特徴:
✅ シンプルで理解しやすい
✅ 広いブラウザサポート
❌ 特殊なオブジェクトを正しく処理できない
❌ 循環参照でエラー
シンプルなデータ構造のコピーはできますが、以下のオブジェクトを含む特殊なオブジェクトのコピーはできません。
- Dateオブジェクト
- 関数
- Symbol
- Map/Set
3. Lodashの_.cloneDeep()
const _ = require('lodash'); const original = { name: "山田", greet: function() { console.log(`こんにちは、${this.name}です`); }, details: { skills: ["JavaScript", "Python"], experience: { years: 5, companies: ["A社", "B社"] } } }; const cloned = _.cloneDeep(original);
特徴:
✅ 関数を含むあらゆる型に対応
✅ 循環参照に対応
❌ 外部ライブラリ必要
❌ バンドルサイズ増加
外部ライブラリの導入が必要です。
最近のモダンな開発ではstructuredClone()が推奨されますが、関数のコピーが必要な場合は_.cloneDeep()が依然として有用です。
4. カスタム実装
function customDeepClone(obj) { // null または プリミティブ値の場合はそのまま返す if (obj === null || typeof obj !== 'object') { return obj; } // 日付オブジェクトの処理 if (obj instanceof Date) { return new Date(obj.getTime()); } // 配列の処理 if (Array.isArray(obj)) { return obj.map(item => customDeepClone(item)); } // Map の処理 if (obj instanceof Map) { return new Map( Array.from(obj.entries()).map( ([key, value]) => [customDeepClone(key), customDeepClone(value)] ) ); } // Set の処理 if (obj instanceof Set) { return new Set( Array.from(obj).map(value => customDeepClone(value)) ); } // 通常のオブジェクトの処理 const clonedObj = {}; Object.entries(obj).forEach(([key, value]) => { clonedObj[key] = customDeepClone(value); }); return clonedObj; } // 使用例 const original = { date: new Date(), map: new Map([['key', 'value']]), set: new Set([1, 2, 3]), nested: { array: [1, 2, {x: 3}] } }; const cloned = customDeepClone(original);
特徴:
✅ 要件に最適化可能
使い分けの指針
モダンブラウザ向けの一般的な使用
structuredClone()
シンプルなデータ構造
JSON.parse/stringify
関数を含むオブジェクト
Lodash cloneDeep
特殊なユースケース
カスタム実装 要件に最適化可能
注意点とベストプラクティス
シャローコピーで十分な場合
- 1階層のオブジェクトの場合
- パフォーマンスが重要な場合
- イミュータブルな操作を行う場合(Reduxのステート更新など)
ディープコピーが必要な場合
- ネストされたオブジェクトの完全な複製が必要な場合
- 元のオブジェクトとの完全な独立性が必要な場合
- データの永続化や保存が必要な場合
まとめ
1. シャローコピー
- 単純な構造のオブジェクトに適している
- パフォーマンスが優れている
- スプレッド構文が最も読みやすく推奨
2. ディープコピー
- 複雑なオブジェクトの完全な複製に必要
- structuredClone()が最新の標準的な方法
- 特殊なケースではLodashなどのライブラリを検討
3. 選択の基準
- オブジェクトの構造の複雑さ
- パフォーマンス要件
- ブラウザのサポート状況
- 特殊なデータ型の有無
おわりに
今回は「JavaScriptのシャローコピーとディープコピー」について詳しく説明してみました!
参考になれば幸いです。
弊社ではエンジニアを募集しております。まずはカジュアル面談でお話ししましょう!
ご興味ありましたら、ぜひご応募ください!
次の記事は私のチームリーダーのまつむらさんです
普段から自宅にサーバーを立ててかわいがっているまつむらさんの記事楽しみです!
iimon採用サイト / Wantedly / Green
参考記事
https://javascript.info/object-copy?locale=ja
https://developer.mozilla.org/ja/docs/Web/API/Window/structuredClone
https://developer.mozilla.org/ja/docs/Glossary/Shallow_copy
https://developer.mozilla.org/ja/docs/Glossary/Deep_copy