JavaScriptにおけるシャローコピーとディープコピーの違い
実務においてシャローコピーとディープコピーの概念を理解していなかったため、
思った通りの挙動にならなく、苦労したのでメモとして残します。
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.jp/recruit/
Discussion
動かしてないのでわからないのですが、再帰だと循環参照とかのときにはまったりするかもしれません。
このあたりで、実装と大量のテストコード書いたりしたことあり、循環参照のテストも書いてます。
これをobject2にcloneDeepする、ってやつです。
独自クラスクローンも関数で定義して設定してくれたらできる、全部自前実装の記事書いた個ともあるのでどうぞです。
JavaScript さまざまな型に対応できる拡張機能つき clone と cloneDeep を実装しました。 - Qiita
また、今は、基本は、structuredClone でよいように思います。
他の方の記事ですが下記に書いてありました。
_.cloneDeepを葬りましょう - Qiita
lodashのcloneDeepも便利ですし低速でもないので問題ないでしょう。(ブラウザ依存でstructuredCloneのない環境の方が怖い。)
上記記事のコメント欄みると独自クラスのインスタンスコピーはできないみたいですが、独自クラスとかJS/TSで使う必要もない(使うべきではないw)ので捨てておけば、よいとも思います。