_.cloneDeepを葬りましょう

はいさい！オースティンやいびーん！

# 概要

JavaScript・ブラウザのネイティブ機能で回帰的クローンをする方法を紹介します！

それは、[`structuredClone`](https://developer.mozilla.org/ja/docs/Web/API/structuredClone)です。

これは、グローバルで定義されている関数で、現在のEvergreenブラウザ（Chrome、Safari、Edge等）ならサポートされています。

https://developer.mozilla.org/ja/docs/Web/API/structuredClone

## 背景

JavaScript開発においてオブジェクトをクローンすることは常の課題です。

常套手段として以下のES6 Spreadがよく使われます。

```javascript
const ob1 = { a: 1 };
const ob1ShallowClone = { ...ob1 }; // OR Object.create(ob1);
```

これはいわゆる**Shallow Clone**の手法です。基本的にこれでもの足りるケースが多いのですが、非常に大きな欠点があります。

それは、**配列のようなオブジェクトは回帰的にクローンされず、[オブジェクト参照](https://developer.mozilla.org/ja/docs/Glossary/Object_reference)がコピーされるにすぎない**というところです。

以下の例で問題が顕著に現れます。

```javascript
const beforeUserInput = {
 title: 'サーバーからのデータ',
 content: '編集されていない',
 tags: ['js', 'ts']
}

const userInput = { ...beforeUserInput };
userInput.tags.push('death-to-lodash');

console.log(beforeUserInput.tags); // ['js', 'ts', 'death-to-lodash']
```

アキサミヨー！これでは元々のデータまで変わってしまう。

そこで、**Deep Clone**が誕生します。

Deep Cloneは回帰的にクローンすることで、オブジェクトの中のオブジェクトをクローンすることです。

## これまでどうやってDeep Cloneをしていたのか？

そうすると、同じオブジェクト参照ではなく、独立したオブジェクトを作ることができて、上記の問題を解決できます。

ここで問題なのですが、**古くからDeep Cloneのネイティブの手段がなかった**のです。

そこで、多くの人はこれまで[Lodashの`_.cloneDeep()`](https://lodash.com/docs/4.17.15#cloneDeep)に頼ってきました。

しかし、この投稿の本題ですが、ネイティブの手段は今だと、あります。

それが、`structuredClone`です。

しかも、それを知らなくても、Service Worker、Web Workerを使っているのなら、既に使っています。

なぜなら、Web Workerから情報をMain Threadに送る時に、`structuredClone`が使われています。

# structuredCloneの使い方

ごくごく簡単です。今でもDevelopers Consoleを開いてやってみてください。

![スクリーンショット 2023-01-28 7.37.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2344956/8c432366-fbcf-1dec-123a-3f001f9e1b38.png)

これだけです。本当に素晴らしい。

# structuredCloneのパフォーマンス

Lodashの_.cloneDeepと比較してみましょう。

```javascript
//@ts-check
import * as _ from 'lodash'

const iterations = 300000;
const baseObject = () => ({
  array1: [1, 2, 3, 4, 5],
  array2: [[1], [2], [3]],
  array3: [[[1]], [[2]], [[3]]],
  obj1: {
    arr1: [[[[1]]], [[[[2]]]], [[[[3]]]]],
  },
});

const lodashCloneLoop = () => {
  for (let index = 0; index < iterations; index++) {
    const obj = baseObject();
    _.cloneDeep(obj);
  }
};

const nativeCloneLoop = () => {
  for (let index = 0; index < iterations; index++) {
    const obj = baseObject();
    structuredClone(obj);
  }
};

/**
 * @param {() => any} func
 */
function timeIt(func) {
  const startTime = Date.now();
  func();
  const endTime = Date.now();
  return endTime - startTime;
}

console.log("lodash cloneDeep time (ms): ", timeIt(lodashCloneLoop));
console.log("native structuredClone time (ms): ", timeIt(nativeCloneLoop));
```

上記のコードをESBuildでコンパイルしてブラウザで実行します。

![スクリーンショット 2023-01-28 7.39.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2344956/c11e49e7-0efe-9234-f815-6fbe9a7a81a8.png)

これだ！やはり、ネイティブが早かったです。なんと25%も処理時間が早いわけです。

# なぜLodashを使うべきでない

使う必要性はほぼほぼないからです。

それに、二つの問題点があります。

- Bundle Sizeが大きくなる
- 基本的にLodashよりES6のネイティブ関数の方が早い
- Lodashを知らない人が読むのに苦労する（[_.partialRight](https://lodash.com/docs/4.17.15#partialRight)は本当に解せない）

唯一、Lodashにあってネイティブ機能にない困るものは、`_.isEqual`。これは便利だなと思いますが、今後、JavaScriptにも[RecordおよびTuple](https://github.com/tc39/proposal-record-tuple)がECMAScriptにも含まれるので、時間の問題です。

# structuredCloneができないこと

structuredCloneは[構造化複製アルゴリズム](https://developer.mozilla.org/ja/docs/Web/API/Web_Workers_API/Structured_clone_algorithm#%E6%A7%8B%E9%80%A0%E5%8C%96%E8%A4%87%E8%A3%BD%E3%81%A7%E5%8B%95%E4%BD%9C%E3%81%97%E3%81%AA%E3%81%84%E3%82%82%E3%81%AE)を使って回帰的クローンを実現しているのですが、クローンできないものがあります。

それは以下の主なものです。

- 関数（() => {}など）
- DOM Node

というので、99.9%の場合は全く気にする必要はないのです。

# まとめ

以上、`structuredClone`を紹介してまいりました。

感動しましたか？僕は感動しています。

もっと早く気づくべきでした、すぐそばに運命の相手がずっといることに。

ぜひstructuredCloneを使っていきましょう！