構造化複製アルゴリズムを使ったオブジェクトのディープコピー
構造化複製アルゴリズムとは
構造化複製アルゴリズム(The structured clone algorithm)とは HTML Standard の中で定義されている JavaScript の値をシリアライズ、デシリアライズするアルゴリズムのことを言います[1]。postMessage や、IndexedDB に格納する際など用いられます。
対応している値
ECMAScript で定義されているものについては
-
Symbolを除くプリミティブ値とプリミティブラッパーオブジェクト Date-
RegExp[2] -
ArrayBuffer,SharedArrayBuffer -
ArrayBufferView(TypedArray,DataView) -
Map,Set -
Error[3] -
NativeError(EvalError,RangeError,ReferenceError,SyntaxError,TypeError,URIError) -
Array, プレーンなObject[4]
が対応しています。対応していない Promise や WeakMap そして Proxy などが含まれる場合は DOMException を投げます。なお ArrayBuffer は後述する [Transferable] と同じように扱われます。
他に Web IDL にて [Serializable] 拡張属性が付与されているものも対応します。具体例として一部列挙すると以下の通りです。
-
Blob,File,FileList CryptoKeyDOMException-
DOMPoint,DOMPointReadOnly,DOMRect,DOMRectReadOnly,DOMQuad,DOMMatrix,DOMMatrixReadOnly -
ImageData,ImageBitmap -
WebAssembly.Module[5]
また [Transferable] 拡張属性が付与されているものは適切にオプションを設定することで転送することが出来ます。postMessage などで送信先に所有権を渡す場合に用いられます。こちらも一部列挙すると以下の通りです。
OffscreenCanvasImageBitmapMessagePort-
ReadableStream,WritableStream,TransformStream
なお一覧は Node.js を使って取得しました。
structuredClone 函数
先日構造化複製アルゴリズムを同期的に扱える structuredClone 函数が HTML Standard に取り込まれました。
将来的にこれを使うことでディープコピーをもっと手軽に実行出来るようになります。
const base = {
foo: "foo",
bar: {
name: "bar",
},
};
base.self = base;
const cloned = structuredClone(base);
console.log(cloned.foo); // => "foo"
console.log(cloned.bar.name); // => "bar"
// ちゃんとディープコピーされる
console.assert(cloned !== base);
console.assert(cloned.bar !== base.bar);
// 循環参照にも対応
console.assert(cloned.self === cloned);
ブラウザの実装
開発者用ビルドで一部実装されています。
今回は珍しく Chrome が一番出遅れているようです。
Node.js
Node.js v17.0.0 から扱えるようになりました。
今のところ MessageChannel を利用した突貫実装になっています。大体以下のような感じです[6]。
import { MessageChannel, receiveMessageOnPort } from "worker_threads";
let channel;
export function structuredClone(value, options = undefined) {
channel ??= new MessageChannel();
channel.port1.unref();
channel.port2.unref();
channel.port1.postMessage(value, options?.transfer);
return receiveMessageOnPort(channel.port2).message;
}
ちなみに特に [Transferable] の対応をする必要がなければ以下のように実装できます。
import { serialize, deserialize } from "v8";
export function clone(value) {
return deserialize(serialize(value));
}
Deno
Deno v1.13.0 から扱えます。
ArrayBuffer の [Transferable] 対応は v1.14.0 からです。
その他の [Serializable] と [Transferable] についてはまだ実装されていません。
core-js (polyfill)
core-js に polyfill が実装されました。[Transferable] については可能なものが一部対応されます。
なお後述する仕様側で遅れている問題も先取りで対応される予定です。また既に structuredClone が実装されている環境で読み込むと、その実装を活かして [Serializable] の対象が増えるように実装されています。
仕様対応の遅れ
ECMAScript
構造化複製アルゴリズムは HTML Standard で定義されているものです。そのため最近の ECMAScript の仕様の変化に対応できていないところがあります。
例えば ES2021 Promise.any で仕様に入った AggregateError に対応できておらず、ただの Error へと変換されてしまいます[7]。
また ES2022 Class Fields で追加された [[PrivateElements]] 内部スロットに対応できていないことを発見したので報告しました(これは実装にはほとんど関係ありません)。
この ECMAScript に追随できていない問題については2021年10月の TC39 会議で議題にあがり、議論されています。
ESNext
今ある提案についても将来的に対応しないといけません。Stage 3 Error Cause や Stage 3 Temporal[8] そして Stage 2 Record & Tuple がそれにあたります。
なお Stage 3 Error Cause と Stage 2 Record & Tuple について詳しく知りたい方は別の記事を用意したので是非御覧ください。
WebAssembly
WebAssembly JS API の仕様によると WebAssembly.{Compile, Link, Runtime}Error も NativeError と同様に扱うと定義されています。そして構造化複製アルゴリズムにおいてまだ対応できていません。こちらも AggregateError や Stage 3 Error Cause と同様に対応中です。
結び
今回は HTML Standard の structuredClone 函数とそれに関連した実装、仕様についての話題を取り上げてみました。便利なので早くブラウザでも扱えるようになって欲しいところです。
仕様追うのは割と楽しいので皆さんもよかったらどうぞ。
-
正確には HTML Standard ではもう構造化複製アルゴリズムとして定義されてはいないようですが、この記事では MDN にあわせてこう記述しています。 ↩︎
-
RegExpインスタンスの持つlastIndexプロパティは保持されません。 ↩︎ -
NativeErrorではないエラーオブジェクトはErrorへと変換されます。AggregateErrorについては後述。 ↩︎ -
オブジェクトの持つ
Symbolキーでない列挙可能なプロパティのみ保持されます。ArrayはRegExp#execの返り値のように integer-indexed ではないプロパティについても対象となります。 ↩︎ -
TODO コメントが付いているものの、いいのかなこれ……と思わなくもない。 ↩︎
-
変換の際に
errorsプロパティも取り除かれてしまうため、実質的に何のエラーなのかわからなくなってしまいます。 ↩︎ -
余談ですが Temporal は IETF でタイムゾーンとカレンダーの文字フォーマットの承認待ちをしている状況で、12月以降に Stage 4 になると思われます。 ↩︎
Discussion