🐶

JavaScriptにおけるシャローコピーとディープコピーの違い

2023/09/13に公開1

実務においてシャローコピーとディープコピーの概念を理解していなかったため、
思った通りの挙動にならなく、苦労したのでメモとして残します。

JavaScriptにおいて、オブジェクトや配列、複合データをコピーする際の
「そのデータのどこまでの層をコピーするのか」という概念について説明します。

シャローコピー

シャローコピーは、データの最上位層のみを新しいメモリー空間にコピーします。
ネストされたオブジェクトや配列は、参照がコピーされるだけとなります。
これにより、シャローコピーしたオブジェクトのネストされた部分を変更すると、
元のデータまで変更されてしまいます。

以下がシャローコピーの実装方法です。

const original = { a: 1, b: { c: 2 } };
const copied = Object.assign({}, original);

console.log(copied); // { "a": 1, "b": { "c": 2 } }
console.log(original); // { "a": 1, "b": { "c": 2 } }

/**
 * コピーしたデータのcの値を変更した場合
 */
copied.b.c = 3;

// コピーデータの出力結果
console.log(copied); // { "a": 1, "b": { "c": 3 } }

// オリジナルデータの出力結果
console.log(original); // { "a": 1, "b": { "c": 3 } }

// コピーデータとオリジナルデータの比較
console.log(copied.b.c === original.b.c); // true 
const original = { a: 1, b: { c: 2 } };
const copied = { ...original };

console.log(copied); // { "a": 1, "b": { "c": 2 } }
console.log(original); // { "a": 1, "b": { "c": 2 } }

/**
 * コピーしたデータのcの値を変更した場合
 */
copied.b.c = 3;

// コピーデータの出力結果
console.log(copied); // { "a": 1, "b": { "c": 3 } }

// オリジナルデータの出力結果
console.log(original); // { "a": 1, "b": { "c": 3 } }

// コピーデータとオリジナルデータの比較
console.log(copied.b.c === original.b.c); // true

const originalArray = [1, 2, [3, 4]];
const copiedArray = originalArray.slice();

console.log(copiedArray); // [1, 2, [3, 4]]
console.log(originalArray); // [1, 2, [3, 4]]

/**
 * コピーした配列内にある配列のデータを変更した場合
 */
copiedArray[2][0] = 5;

// コピーデータの出力結果
console.log(copiedArray); // [1, 2, [5, 4]]

// オリジナルデータの出力結果
console.log(originalArray); // [1, 2, [5, 4]]

// コピーデータとオリジナルデータの比較
console.log(copiedArray[2][0] === originalArray[2][0]); // true

const originalArray = [1, 2, [3, 4]];
const copiedArray = [].concat(originalArray);

console.log(copiedArray); // [1, 2, [3, 4]]
console.log(originalArray); // [1, 2, [3, 4]]

/**
 * コピーした配列内にある配列のデータを変更した場合
 */
copiedArray[2][0] = 5;

// コピーデータの出力結果
console.log(copiedArray); // [1, 2, [5, 4]]

// オリジナルデータの出力結果
console.log(originalArray); // [1, 2, [5, 4]]

// コピーデータとオリジナルデータの比較
console.log(copiedArray[2][0] === originalArray[2][0]); // true

シャローコピーの場合、ネストされた情報を変更しようとした際に、
元のデータも変更されてしまうため、ネストされたデータを変更する可能性がある場合
ディープコピーを使用する必要があります。

ディープコピー

ディープコピーは、データの全ての層、つまりネストされたオブジェクトや配列まで
新しいメモリ空間にコピーします。
ディープコピーを行うと、元のデータとコピーされたデータは完全に独立し、
一方を変更しても他方に影響はありません。

JSONを使用する方法:

const deepCopy = obj => JSON.parse(JSON.stringify(obj));

しかし、この方法には注意が必要です。
関数、undefined、Symbolなど、JSONでサポートされていないデータ型はコピーできません。

再帰を使用する方法:

/**
 * ディープコピーの作成
 *
 * @param Object source //  コピー元のオブジェクト。
 * @param WeakMap alreadyCopy // 既にコピーしたオブジェクトの追跡
 * @returns Object // コピーされたオブジェクト。
 */
function deepCopy(source, alreadyCopy = new WeakMap()) {
    // プリミティブな値(文字列、数値、真偽値など)、null、またはオブジェクトでない場合、そのまま返す
    if (source === null || typeof source !== 'object') return source;

    // 既にコピーしたオブジェクトの場合、そのコピーを返す
    if (alreadyCopy.has(source)) return alreadyCopy.get(source);

    // 特定のオブジェクト型の場合の処理
    // 正規表現オブジェクト
    if (source instanceof RegExp) return new RegExp(source);
    // 日付のオブジェクト
    if (source instanceof Date) return new Date(source);

    // 新しいオブジェクトのインスタンスを作成
    const copy = new source.constructor();
    // コピーの追跡
    alreadyCopy.set(source, copy);

    // すべてのプロパティを再帰的にコピー
    for (const key in source) {
        if (source.hasOwnProperty(key)) {
            copy[key] = deepCopy(source[key], alreadyCopy);
        }
    }
    
    return copy;
}

const original = {
    a: 1,
    b: "hello",
    c: [1, 2, 3]
};

const copied = deepCopy(original);

console.log(copied);  // {"a": 1, "b": "hello", "c": [1, 2, 3]}
console.log(original);  // {"a": 1, "b": "hello", "c": [1, 2, 3]}

/**
 * コピーしたデータのcの値を変更した場合
 */
copied.c.[0] = 3;

// コピーデータの出力結果
console.log(copied);  // {"a": 1, "b": "hello", "c": [3, 2, 3]}

// オリジナルデータの出力結果
console.log(original); // {"a": 1, "b": "hello", "c": [1, 2, 3]}

// コピーデータとオリジナルデータの比較
console.log(original.c.[0] === copied.c.[0]); // false

外部ライブラリを使用する方法:

const _ = require('lodash');
const copied = _.cloneDeep(original);

プロジェクトによってどの方法を採用するかは、精査する必要があります。

まとめ

シャローコピーとディープコピーの概念をしっかり理解することで
オブジェクトや配列のコピーを行う際に思わぬエラーを招く可能性を防ぐことができます!
正しく理解して、どちらの方法がベストか考えて使用しましょう!

Arsaga Developers Blog

Discussion

standard softwarestandard software

動かしてないのでわからないのですが、再帰だと循環参照とかのときにはまったりするかもしれません。

このあたりで、実装と大量のテストコード書いたりしたことあり、循環参照のテストも書いてます。
https://github.com/standard-software/partsjs/blob/7cc194d8fd9c0f3fda8a8e4432989d333200e3b2/source/common/common.test.js#L1119

        const object1 = { b: 'test' };
        object1.a = object1;

これをobject2にcloneDeepする、ってやつです。

独自クラスクローンも関数で定義して設定してくれたらできる、全部自前実装の記事書いた個ともあるのでどうぞです。
JavaScript さまざまな型に対応できる拡張機能つき clone と cloneDeep を実装しました。 - Qiita
https://qiita.com/standard-software/items/54bf2284ae1833a786d7

また、今は、基本は、structuredClone でよいように思います。

他の方の記事ですが下記に書いてありました。

_.cloneDeepを葬りましょう - Qiita
https://qiita.com/tronicboy/items/963faf0b162343e47223

lodashのcloneDeepも便利ですし低速でもないので問題ないでしょう。(ブラウザ依存でstructuredCloneのない環境の方が怖い。)

上記記事のコメント欄みると独自クラスのインスタンスコピーはできないみたいですが、独自クラスとかJS/TSで使う必要もない(使うべきではないw)ので捨てておけば、よいとも思います。