12
6

More than 1 year has passed since last update.

_.cloneDeepを葬りましょう

Last updated at Posted at 2023-01-27

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

概要

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

それは、structuredCloneです。

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

背景

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

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

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

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

それは、配列のようなオブジェクトは回帰的にクローンされず、オブジェクト参照がコピーされるにすぎないというところです。

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

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()に頼ってきました。

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

それが、structuredCloneです。

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

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

structuredCloneの使い方

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

スクリーンショット 2023-01-28 7.37.15.png

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

structuredCloneのパフォーマンス

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

//@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

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

なぜLodashを使うべきでない

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

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

  • Bundle Sizeが大きくなる
  • 基本的にLodashよりES6のネイティブ関数の方が早い
  • Lodashを知らない人が読むのに苦労する(_.partialRightは本当に解せない)

唯一、Lodashにあってネイティブ機能にない困るものは、_.isEqual。これは便利だなと思いますが、今後、JavaScriptにもRecordおよびTupleがECMAScriptにも含まれるので、時間の問題です。

structuredCloneができないこと

structuredCloneは構造化複製アルゴリズムを使って回帰的クローンを実現しているのですが、クローンできないものがあります。

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

  • 関数(() => {}など)
  • DOM Node

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

まとめ

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

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

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

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

12
6
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
6