この記事で分かること
- ディープコピーの定義
- 様々なディープコピーの手法
1.背景
以下の記事でstructuredCloneの存在を知った為です。
2. ディープコピーとは
コピー先のオブジェクトのプロパティがコピー元のオブジェクトのプロパティと同一の参照(同じ値を指す)を共有しないコピーです。
3. ディープコピーを作成する方法
3.1. JSON.parse(JSON.stringify(オブジェクト))
JSON.stringify() でオブジェクトを JSON 文字列に変換しJSON.parse() で文字列から(完全に新しい) JavaScript のオブジェクトに変換します。
const originalObj = {key:'originalValue'};
const deepcopy = JSON.parse(JSON.stringify(originalObj));
deepcopy.key = 'deepcopyValue';
console.log(originalObj.key);
// originalValue
欠点
- 再帰的なデータ構造を指定するとJSON.stringify() は例外をthrowします
再帰的なデータ構造の例は以下です。
class Node {
constructor(data, next = null) {
this.data = data;
this.next = next;
}
}
const node1 = new Node(1);
const node2 = new Node(2);
const node3 = new Node(3);
node1.next = node2;
node2.next = node3;
node3.next = node1;
// node1 -> node2 -> node3 -> node1 -> node2 -> node3 -> node1 ->...
これをディープコピーしようとすると確かに以下のようにErrorになります。
const deepcopy = JSON.parse(JSON.stringify(node1));
VM707:1 Uncaught TypeError: Converting circular structure to JSON
--> starting at object with constructor 'Node'
| property 'next' -> object with constructor 'Node'
| property 'next' -> object with constructor 'Node'
--- property 'next' closes the circle
at JSON.stringify (<anonymous>)
at <anonymous>:1:34
web.devでは「組み込み型で
Map、Set、Date、RegExp、ArrayBuffer などの他の JS 組み込みが含まれている場合にJSON.stringify() は例外をスローします」
と記述がありましたが、例外は throw されませんでした。
僕の解釈が誤っていたらコメントでご教授ください🙏
問題なければweb.devに質問してみたいと思います。
const map = new Map();
JSON.stringify(map);
// '{}'
const set = new Set();
JSON.stringify(set);
// '{}'
const date = new Date();
JSON.stringify(date);
// '"2024-01-07T06:37:26.243Z"'
const regExp = new RegExp('a');
JSON.stringify(regExp);
// '{}'
const buffer = new ArrayBuffer(10);
JSON.stringify(buffer);
// '{}'
- JSON.stringify() は関数を静かに破棄(例外をthrowせず関数のプロパティを無視)します。
const myObject = {
key: 'value',
myFunction: () => {
console.log('This is a function.');
}
};
const jsonString = JSON.stringify(myObject);
console.log(jsonString);
// {"key":"value"}
3.2. structuredClone
Structured Clone Algorithm を使用して、指定された値のディープ・クローンを作成します。
structuredCloneの歴史について以下の記事で紹介されていましたのでご覧ください。
structuredCloneの利点
- SetやMapなどのオブジェクトもコピーできる
const map = new Map();
structuredClone(map);
// Map(0) {size: 0}
const set = new Set();
structuredClone(set);
// Set(0) {size: 0}
const regExp = new RegExp('a');
structuredClone(regExp);
// /a/
const buffer = new ArrayBuffer(10);
structuredClone(buffer);
// ArrayBuffer(10)
- 循環したオブジェクトをコピーできる
class Node {
constructor(data, next = null) {
this.data = data;
this.next = next;
}
}
const node1 = new Node(1);
const node2 = new Node(2);
const node3 = new Node(3);
node1.next = node2;
node2.next = node3;
node3.next = node1;
// node1 -> node2 -> node3 -> node1 -> node2 -> node3 -> node1 ->...
これをディープコピーしたのが以下です。
structuredCloneの欠点
- JSON.parse(JSON.stringify(オブジェクト))の時と同様に関数はディープコピーできません。
違う点は例外がthrowされます。
const func = () => 123;
const func2 = structuredClone(func);
// Uncaught DOMException: Failed to execute 'structuredClone' on 'Window': () => 123 could not be cloned.
- 独自のクラスのインスタンスをコピーした際にPrototypeが何かという情報が失われる
class MyClass {
foo = 1;
}
const obj = new MyClass();
const obj2 = structuredClone(obj);
obj2.foo // => 1
obj2 instanceof MyClass // => false
- Symbolをコピーできない
const sym = Symbol("foo");
structuredClone(sym);
//Uncaught DOMException: Failed to execute 'structuredClone' on 'Window': Symbol(foo) could not be cloned.
3.3 LodashのcloneDeep
こちらは他記事で記載されていますので割愛します。
バンドルサイズを小さくする方法があるそうです。
import _delay from 'lodash/delay';
3.4 just-clone
他記事のコメントで拝見しました。
バンドルサイズが lodash より小さいそうです。
just-clone
lodash
lodashと同様に関数のコピーが可能、独自classのインスタンスのコピーは不可能です。
4. 最後に
- JSON.parse(JSON.stringify(オブジェクト)) の欠点の多くをカバーできるstructuredCloneを積極的に使いたいなと思いました。
ただし、上で説明した欠点を気にしないで良い場面であることが条件です。 - 参考になる記事を書いてくださった皆様に感謝致します!
- 話は脱線しますが、業務でJQueryを使っているのでJQueryのバンドルサイズを小さくする方法を調べたいなと思いました。