
はじめに
こんにちは、株式会社iimonでエンジニアをしている中村です。 業務では、主にフロントエンドを担当させていただいています。 今回は「シャローコピーとディープコピーの違い」について聞いたことはありますが、よくわかっていなかったので調べてみました。
結論
シャローコピーとディープコピーは、オブジェクトのコピーを作成する二つの異なる方法です。シャローコピーは参照アドレスのコピー、ディープコピーはオブジェクト自体のコピーのことを指します。
オブジェクトのメモリ参照イメージ
そもそも、オブジェクトとは値を格納している場所(メモリ上のアドレス)を 格納します。
const Taro = {age: 20, job: "engineer"}
図のようにオブジェクトはそれぞれ持つプロパティ(age,job)はその値への参照を保持しています。
ちなみに、以下の例はシャローコピーではないようです。オブジェクトの参照を単純に別の変数に割り当てているだけで、新しいオブジェクトの作成は行われていません。
const Taro = {age: 20, job: "engineer"}
const copiedTaro = Taro; // 参照をコピー
copiedTaro.age = 30; // 値を代入
console.log(Taro.age); // 30

シャローコピー
シャローコピーは、浅いコピーです。シャローコピーの場合には、オブジェクトの参照をコピーし、オブジェクトの値はコピーされません。
オブジェクトの最上位のプロパティは新しいオブジェクトにコピーされますが、プロパティがさらにオブジェクトを指している場合(ネストされたオブジェクト)、それらの内部オブジェクトはコピーされず、元のオブジェクトへの参照として残ります。

const Taro = {age: 20, nested: { job: "engineer"}};
const copiedTaro = Object.assign({}, Taro);
//最上位のプロパティの変更
copiedTaro.age = 30;
//2階層目の変更
copiedTaro.nested.job = "teacher";
console.log(Taro);
// { age: 20, nested: { job: 'teacher' } }
console.log(copiedTaro);
// { age: 30, nested: { job: 'teacher' } }
このようにcopiedTaroオブジェクトの最上位のプロパティageは値のコピーをしているため、copiedTaro固有の値を持つことができます。しかし、ネストされたオブジェクトjobは元のオブジェクトTaroの参照を保持するため、copiedTaro.nested.job = "teacher";の影響がTaroにも出てしまうことがわかります。
シャローコピーの使用方法
JavaScriptでは、すべての標準組込みオブジェクトのコピー操作において、ディープコピーではなくシャローコピーを生成します。
Object.assign()Object.assign() メソッドは、すべての列挙可能なプロパティの値を、一つのターゲットオブジェクトにコピーするために使用されます。Object.create()Object.create() メソッドは、既存のオブジェクトを新しく生成されるオブジェクトのプロトタイプとして使用して、新しいオブジェクトを生成します。
配列メソッドでもシャローコピーは使用されます。以下のような方法があります。
スプレッド構文配列やオブジェクトリテラル内での要素の展開に使われます。配列の場合、新しい配列を作成する際に既存の配列の各要素を個別の要素として取り出すことができます。Array.prototype.concat()concat() メソッドは、既存の配列に新しい要素を連結して新しい配列を作成します。Array.prototype.slice()slice() メソッドは、配列の一部を浅くコピーして新しい配列オブジェクトを作成します。引数なしで使用すると配列全体がコピーされます。
配列でも同様です。
最上位プロパティcopyArray[1]は値のコピーをしているため、copyArray固有の値を持つことができます。
しかし、ネストされた配列は同じ参照先を指すので、copyArray[0][0] = 0;の変更がoriginalArrayにも反映されていることがわかります。
const originalArray = [[1], 2, 3]; // シャローコピー const copyArray = [...originalArray]; copyArray[1] = 0; // 1階層目の変更 copyArray[0][0] = 0; // ネストされた配列の変更 console.log(originalArray); // [ [ 0 ], 2, 3 ] console.log(copyArray); // [ [ 0 ], 0, 3 ]
ディープコピー
ディープコピーは、深いコピーです。ディープコピーの場合には、オブジェクトの保持する値も複製して、別の独立したオブジェクトを新たに作成します。
const Taro = {age: 20, nested: {job: "engineer"}}
const copiedTaro = JSON.parse(JSON.stringify(Taro))
// ネストされてオブジェクトのプロパティを変更
copiedTaro.nested.job = "teacher"
console.log(Taro);
// {age: 20, nested: {job: "engineer"}}
console.log(copiedTaro);
// {age: 20, nested: {job: "teacher"}}
先ほどは、コピーしたjobプロパティに値を代入するとコピー元のTaroに影響していましたが、ディープコピーを使用すると、影響は出ません。このようにディープコピーでは、ネストのプロパティを独立させて持つことができます。
ディープコピーの使用方法
ディープコピーのやり方として一般的なのが、JSON.parse(JSON.stringify(object))です。
これは、対象オブジェクトが JSON文字列化可能であれば JSON.stringify() でオブジェクトを JSON 文字列に変換し、 JSON.parse() で文字列からJavaScript のオブジェクトに変換することができます。
しかし、この方法にはいくつか限界があります。関数、undefined、シンボル、また循環参照があるオブジェクトはJSON文字列化できないため適切にコピーできません。
const objWithFunctions = {
a: function() { console.log("Hello"); },
b: undefined,
c: Symbol("c")
};
const deepCopyWithFunctions = JSON.parse(JSON.stringify(objWithFunctions));
console.log(deepCopyWithFunctions);
出力: {}
関数、undefined、シンボルはJSON形式で表現できないため、a,b,cのプロパティはコピーされません。
// 循環参照を含むオブジェクトの例
const CircularReference = {};
CircularReference.self = CircularReference;
try {
const deepCopyWithCircularReference = JSON.parse(JSON.stringify(CircularReference));
} catch (error) {
console.error("Error:", error.message);
}
出力:Error: Converting circular structure to JSON
この出力はディープコピーのプロセスがオブジェクトの参照を辿る際に、循環参照ではループを起こしてしまうため、エラーを起こします。最終的にスタックオーバーフローやシステムリソースの枯渇を起こす可能性があります。 他の方法で、関数やundefinedのディープコピーを行いたい場合はNode.jsにLodashライブラリをインポートしてカスタムコピー関数と組み合わせる方法などがあるそうなので、今後調べていこうと思います。
最後に
オブジェクトの最上位層のコピーを行いたい場合は、シャローコピーを使用し、ネストを含んだ完全なオブジェクトをコピーしたい場合は、ディープコピーを使用すればいいことがわかりました。ディープコピーはオブジェクトに合わせて適宜使用するメソッドを変更していこうと思います。ご覧いただきありがとうございました。
iimonではエンジニアを募集しています。カジュアルからでもお話させていただきたく、是非ご応募していただけると嬉しいです!
参考記事
Deep copy (ディープコピー) - MDN Web Docs 用語集: ウェブ関連用語の定義 | MDN
Shallow copy (シャローコピー) - MDN Web Docs 用語集: ウェブ関連用語の定義 | MDN
[JavaScript]色々なディープコピー #JavaScript - Qiita