構造化複製アルゴリズムを使ったオブジェクトのディープコピー
構造化複製アルゴリズムとは
構造化複製アルゴリズム(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
CryptoKey
DOMException
-
DOMPoint
,DOMPointReadOnly
,DOMRect
,DOMRectReadOnly
,DOMQuad
,DOMMatrix
,DOMMatrixReadOnly
-
ImageData
,ImageBitmap
-
WebAssembly.Module
[5]
また [Transferable]
拡張属性が付与されているものは適切にオプションを設定することで転送することが出来ます。postMessage
などで送信先に所有権を渡す場合に用いられます。こちらも一部列挙すると以下の通りです。
OffscreenCanvas
ImageBitmap
MessagePort
-
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