iimon TECH BLOG

iimonエンジニアが得られた経験や知識を共有して世の中をイイモンにしていくためのブログです

JavaScriptのシャローコピーとディープコピー詳細解説:実践的な使い方と注意点

はじめに

こんにちは、株式会社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