はじめに
JavaScriptでオブジェクトを扱う際、そのコピー方法は重要な考慮事項です。
特に、ネストされたオブジェクト構造を持つ場合、適切なコピー方法を選択することが重要です。
この記事では、シャローコピーとディープコピー(特にJSONを使用した方法)の違いについて詳しく説明します。
シャローコピーの仕様
- シャローコピー(shallowCopy) = 浅いコピー
シャローコピーは、オブジェクトの最上位のプロパティのみをコピーします。
ネストされたオブジェクトについては、その参照のみがコピーされ、実際のデータはコピーされません。
例えば:
const original = {
a: 1,
b: {
c: 2 // <= bの持つ値がネストされたオブジェクト構造になっている
}
};
const shallowCopy = { ...original }; // スプレッド構文でオブジェクトを展開しコピーする
shallowCopy.b.c = 3; // originalを複製した`shallowCopy`オブジェクトのcを書き換えてみる
console.log(original.b.c); // 3 (元のオブジェクトまで変更されてしまう)
この例では、shallowCopy.b.c
を変更すると、original.b.c
も変更されてしまいます。
つまり破壊的変更になってしまっている。
これは、b
オブジェクトの参照がコピー元
とコピー先
の間で共有されているためです。
共有されたオブジェクト:
- オブジェクト値は、コピー元(親)とコピー先(子)の間で共有されます
- 両方のオブジェクトが同じメモリ上のオブジェクトを指すことになります
// コピー元のオブジェクト
const original = {
a: 1,
b: {
c: 2 --┐ // このオブジェクトへの参照が共有される
} │
}; │
│
// コピー先のオブジェクト
const shallowCopy = {
a: 1,
b: { │ // 同じオブジェクトへの参照
c: 2 --┘
}
};
// 共有されるオブジェクト(相互に影響)
{
c: 2
}
アドレス 内容
-------- ------------------
0x001 { b: 0x002 } // 元のオブジェクト
0x002 { c: 2 } // ネストされたオブジェクト
0x003 { b: 0x002 } // シャローコピーされたオブジェクト
JSONを使用したディープコピー
- ディープコピー(Deep Copy)= 深いコピー
ディープコピーでは、オブジェクトのすべての階層が新しいメモリ領域にコピーされます。そのため、コピー後のオブジェクトは完全に独立したものとなります。
JSONを利用したディープコピーの例
const original = {
a: 1,
b: {
c: 2 // <= bの持つ値がネストされたオブジェクト構造になっている
}
};
const deepCopy = JSON.parse(JSON.stringify(original));
deepCopy.b.c = 3;
console.log(original.b.c); // 2 (元の`original`オブジェクトは変更されない)
この方法では、JSON.stringify() によりオブジェクトが文字列化され、JSON.parse() により新しいオブジェクトとして再構築されるため、参照の共有が発生しません。
JSONを利用したディープコピーの仕組み
- オブジェクトを文字列化する(JSON.stringify())
- オブジェクト全体をJSON形式の文字列に変換し、メモリ上でフラットなデータにする
- 新しいオブジェクトとして再構築する(JSON.parse())
- 文字列データを元に、新しいメモリ領域を確保してオブジェクトを作成する
- 元のオブジェクトとの参照関係が完全に切断される
- ネストされたプロパティも新しいオブジェクトとして作成されるため、元のオブジェクトを変更してもコピー側には影響しない
主な違い
シャローコピーとディープコピーの比較
項目 | shallowCopy | JSONを使用したDeepCopy |
---|---|---|
データの独立性 | ネストされたオブジェクトの参照が共有される | オブジェクトのすべての階層が新しいメモリ領域で独立 |
メモリ使用量 | ネストされたオブジェクトに新しいメモリは割り当てられない(参照の共有) | オブジェクトのすべてのデータが新しいメモリ領域にコピーされる |
パフォーマンス | 高速 | JSON変換によるオーバーヘッドがある |
JSON方式の注意点
JSONを使用したディープコピーには、いくつかの制約があります。
制約
- 関数、undefined、Symbol はコピーされない
const obj = {
a: 1,
b: undefined,
c: function() { console.log('Hello'); }
};
const copy = JSON.parse(JSON.stringify(obj));
console.log(copy); // { a: 1 } (bとcは消える)
- Date オブジェクトは文字列に変換される
const obj = { date: new Date() };
const copy = JSON.parse(JSON.stringify(obj));
console.log(copy.date); // "2025-02-16T12:00:00.000Z"
- 循環参照を持つオブジェクトはエラーになる
const obj = {};
obj.self = obj;
JSON.parse(JSON.stringify(obj)); // TypeError: Converting circular structure to JSON
解決策
JSONの制限を回避するため、以下の方法があります。
- structuredClone()(ネイティブAPI, 推奨)
const copy = structuredClone(original);
- Lodash の cloneDeep()
const _ = require('lodash');
const deepCopy = _.cloneDeep(original);
まとめ
シャローコピーとディープコピーは、それぞれ異なる用途に適しています。
- シャローコピー
- 高速
- メモリ効率が良い
- ネストされたオブジェクトは共有される
- スプレッド構文 (...)で簡単に実装可能
- JSONを使用したディープコピー
- すべてのプロパティが新しいメモリ領域にコピーされる
- 元のオブジェクトとの参照関係が切断される
- 関数や undefined などが失われる
- JSON.stringify() + JSON.parse() によるオーバーヘッドが発生
最適なコピー方法を選択するためには、コピーするオブジェクトの構造や用途、パフォーマンス要件を考慮することが重要です。
例えば、小規模なオブジェクトでは JSON を使った方法が手軽ですが、関数や Date を含むオブジェクトをコピーする場合はうまくいかないので structuredClone() や _.cloneDeep() の利用を検討しましょう。