はいさい!オースティンやいびーん!
概要
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を開いてやってみてください。
これだけです。本当に素晴らしい。
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でコンパイルしてブラウザで実行します。
これだ!やはり、ネイティブが早かったです。なんと25%も処理時間が早いわけです。
なぜLodashを使うべきでない
使う必要性はほぼほぼないからです。
それに、二つの問題点があります。
- Bundle Sizeが大きくなる
- 基本的にLodashよりES6のネイティブ関数の方が早い
- Lodashを知らない人が読むのに苦労する(_.partialRightは本当に解せない)
唯一、Lodashにあってネイティブ機能にない困るものは、_.isEqual
。これは便利だなと思いますが、今後、JavaScriptにもRecordおよびTupleがECMAScriptにも含まれるので、時間の問題です。
structuredCloneができないこと
structuredCloneは構造化複製アルゴリズムを使って回帰的クローンを実現しているのですが、クローンできないものがあります。
それは以下の主なものです。
- 関数(() => {}など)
- DOM Node
というので、99.9%の場合は全く気にする必要はないのです。
まとめ
以上、structuredClone
を紹介してまいりました。
感動しましたか?僕は感動しています。
もっと早く気づくべきでした、すぐそばに運命の相手がずっといることに。
ぜひstructuredCloneを使っていきましょう!