JavaScript でのディープコピーを理解する

### この記事で分かること
- ディープコピーの定義
- 様々なディープコピーの手法

# 1.背景

以下の記事でstructuredCloneの存在を知った為です。

https://qiita.com/niku_ypp/items/ab01815a63763323d330#%E9%85%8D%E5%88%97%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%82%92%E3%83%87%E3%82%A3%E3%83%BC%E3%83%97%E3%82%B3%E3%83%94%E3%83%BC2

# 2. ディープコピーとは

https://developer.mozilla.org/ja/docs/Glossary/Deep_copy

コピー先のオブジェクトのプロパティがコピー元のオブジェクトのプロパティと同一の参照（同じ値を指す）を共有しないコピーです。

# 3. ディープコピーを作成する方法
## 3.1. JSON.parse(JSON.stringify(オブジェクト))
JSON.stringify() でオブジェクトを JSON 文字列に変換しJSON.parse() で文字列から（完全に新しい） JavaScript のオブジェクトに変換します。
```js
const originalObj = {key:'originalValue'};
const deepcopy = JSON.parse(JSON.stringify(originalObj));
deepcopy.key = 'deepcopyValue';
console.log(originalObj.key);
// originalValue
```
### 欠点 
- 再帰的なデータ構造を指定するとJSON.stringify() は例外をthrowします
再帰的なデータ構造の例は以下です。

```js:リンクされたリスト
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
```

:::note
web.devでは「組み込み型で
Map、Set、Date、RegExp、ArrayBuffer などの他の JS 組み込みが含まれている場合にJSON.stringify() は例外をスローします」
と記述がありましたが、例外は throw されませんでした。
僕の解釈が誤っていたらコメントでご教授ください🙏
問題なければweb.devに質問してみたいと思います。

```js:Map
const map = new Map();
JSON.stringify(map);
// '{}'
```

```js:Set
const set = new Set();
JSON.stringify(set);
// '{}'
```

```js:Date
const date = new Date();
JSON.stringify(date);
// '"2024-01-07T06:37:26.243Z"'
```

```js:RegExp
const regExp = new RegExp('a');
JSON.stringify(regExp);
// '{}'
```

```js:ArrayBuffer
const buffer = new ArrayBuffer(10);
JSON.stringify(buffer);
// '{}'
```
:::

- JSON.stringify() は関数を静かに破棄（例外をthrowせず関数のプロパティを無視）します。
```js
const myObject = {
  key: 'value',
  myFunction: () => {
    console.log('This is a function.');
  }
};

const jsonString = JSON.stringify(myObject);
console.log(jsonString);
// {"key":"value"}
```

## 3.2. structuredClone

https://developer.mozilla.org/en-US/docs/Web/API/structuredClone

Structured Clone Algorithm を使用して、指定された値のディープ・クローンを作成します。

structuredCloneの歴史について以下の記事で紹介されていましたのでご覧ください。

https://zenn.dev/uhyo/articles/what-is-structuredclone#web-api%E3%81%AE%E3%81%A1%E3%82%87%E3%81%A3%E3%81%A8%E3%81%97%E3%81%9F%E5%88%A9%E7%82%B9

### structuredCloneの利点
- SetやMapなどのオブジェクトもコピーできる
```js:Map
const map = new Map();
structuredClone(map);
// Map(0) {size: 0}
```

```js:Set
const set = new Set();
structuredClone(set);
// Set(0) {size: 0}
```

```js:RegExp
const regExp = new RegExp('a');
structuredClone(regExp);
// /a/
```

```js:ArrayBuffer
const buffer = new ArrayBuffer(10);
structuredClone(buffer);
// ArrayBuffer(10)
```

- 循環したオブジェクトをコピーできる
```js:リンクされたリスト
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 ->...
```
これをディープコピーしたのが以下です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405182/0523afd4-9abf-d156-0682-24e97ccc8050.png)

### structuredCloneの欠点
- JSON.parse(JSON.stringify(オブジェクト))の時と同様に関数はディープコピーできません。
違う点は例外がthrowされます。
```js
const func = () => 123;
const func2 = structuredClone(func);
// Uncaught DOMException: Failed to execute 'structuredClone' on 'Window': () => 123 could not be cloned.
```

- 独自のクラスのインスタンスをコピーした際にPrototypeが何かという情報が失われる
```js
class MyClass {
  foo = 1;
}

const obj = new MyClass();

const obj2 = structuredClone(obj);
obj2.foo // => 1
obj2 instanceof MyClass // => false
```
- Symbolをコピーできない
```js
const sym = Symbol("foo");
structuredClone(sym);
//Uncaught DOMException: Failed to execute 'structuredClone' on 'Window': Symbol(foo) could not be cloned.
```

## 3.3 LodashのcloneDeep
こちらは他記事で記載されていますので割愛します。

バンドルサイズを小さくする方法があるそうです。

https://qiita.com/dropmo/items/eee670507187bd03fd6c

```js:lodash一部の関数を使う時
import _delay from 'lodash/delay';
```

## 3.4 just-clone
他記事のコメントで拝見しました。

https://github.com/angus-c/just#just-clone

バンドルサイズが lodash より小さいそうです。

just-clone

https://bundlephobia.com/package/just-clone@6.2.0

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405182/d42baf4d-d940-8b15-e164-3801fb64bd67.png)

lodash

https://bundlephobia.com/package/lodash@4.17.21

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405182/07e7d238-3da6-ffda-053e-3a02f17ef134.png)


lodashと同様に関数のコピーが可能、独自classのインスタンスのコピーは不可能です。



# 4. 最後に
-  JSON.parse(JSON.stringify(オブジェクト)) の欠点の多くをカバーできるstructuredCloneを積極的に使いたいなと思いました。
ただし、上で説明した欠点を気にしないで良い場面であることが条件です。
- 参考になる記事を書いてくださった皆様に感謝致します！
- 話は脱線しますが、業務でJQueryを使っているのでJQueryのバンドルサイズを小さくする方法を調べたいなと思いました。

# 参考
- [構造化クローンを使用した JavaScript でのディープコピー](https://web.dev/articles/structured-clone?hl=ja)
- [structuredCloneはどんなものか](https://zenn.dev/uhyo/articles/what-is-structuredclone)
- [【JavaScript】オブジェクトのディープコピーが標準機能で使えるようになりました](https://zenn.dev/akkie1030/articles/js-structured-clone)