🧬

構造化複製アルゴリズムを使ったオブジェクトのディープコピー

2021/09/21に公開

構造化複製アルゴリズムとは

構造化複製アルゴリズム(The structured clone algorithm)とは HTML Standard の中で定義されている JavaScript の値をシリアライズ、デシリアライズするアルゴリズムのことを言います[1]postMessage や、IndexedDB に格納する際など用いられます。

https://developer.mozilla.org/ja/docs/Web/API/Web_Workers_API/Structured_clone_algorithm

対応している値

ECMAScript で定義されているものについては

  • Symbol を除くプリミティブ値とプリミティブラッパーオブジェクト
  • Date
  • RegExp[2]
  • ArrayBuffer, SharedArrayBuffer
  • ArrayBufferView (TypedArray, DataView)
  • Map, Set
  • Error[3]
  • NativeError (EvalError, RangeError, ReferenceError, SyntaxError, TypeError, URIError)
  • Array, プレーンな Object[4]

が対応しています。対応していない PromiseWeakMap そして 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 を使って取得しました。

https://gist.github.com/petamoriken/3802602b8e93d89e5b4c21e36683cadb

structuredClone 函数

先日構造化複製アルゴリズムを同期的に扱える structuredClone 函数が HTML Standard に取り込まれました。

https://twitter.com/domenic/status/1420071229768224775

https://html.spec.whatwg.org/#dom-structuredclone

将来的にこれを使うことでディープコピーをもっと手軽に実行出来るようになります。

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);

ブラウザの実装

開発者用ビルドで一部実装されています。

https://caniuse.com/mdn-api_structuredclone

今回は珍しく Chrome が一番出遅れているようです。

https://twitter.com/DasSurma/status/1444196712269111297

Node.js

Node.js v17.0.0 から扱えるようになりました。

https://github.com/nodejs/node/pull/40119

今のところ 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 から扱えます。

https://zenn.dev/magurotuna/articles/deno-release-note-1-13-0#2.-self.structuredclone()-のサポート

ArrayBuffer[Transferable] 対応は v1.14.0 からです。

https://zenn.dev/magurotuna/articles/deno-release-note-1-14-0#5.-arraybuffer-がコピーなしでワーカー間を移動できるように

その他の [Serializable][Transferable] についてはまだ実装されていません。

https://github.com/denoland/deno/issues/12067

core-js (polyfill)

core-js に polyfill が実装されました。[Transferable] については可能なものが一部対応されます。

https://github.com/zloirock/core-js/pull/984

なお後述する仕様側で遅れている問題も先取りで対応される予定です。また既に structuredClone が実装されている環境で読み込むと、その実装を活かして [Serializable] の対象が増えるように実装されています。

仕様対応の遅れ

ECMAScript

構造化複製アルゴリズムは HTML Standard で定義されているものです。そのため最近の ECMAScript の仕様の変化に対応できていないところがあります。

例えば ES2021 Promise.any で仕様に入った AggregateError に対応できておらず、ただの Error へと変換されてしまいます[7]

https://github.com/whatwg/html/issues/5716

また ES2022 Class Fields で追加された [[PrivateElements]] 内部スロットに対応できていないことを発見したので報告しました(これは実装にはほとんど関係ありません)。

https://github.com/whatwg/html/issues/7123

この ECMAScript に追随できていない問題については2021年10月の TC39 会議で議題にあがり、議論されています。

https://github.com/tc39/ecma262/issues/2555

ESNext

今ある提案についても将来的に対応しないといけません。Stage 3 Error Cause や Stage 3 Temporal[8] そして Stage 2 Record & Tuple がそれにあたります。

https://github.com/whatwg/html/pull/5749

https://github.com/tc39/proposal-temporal/issues/548

https://github.com/whatwg/html/pull/6958

なお Stage 3 Error Cause と Stage 2 Record & Tuple について詳しく知りたい方は別の記事を用意したので是非御覧ください。

https://zenn.dev/petamoriken/articles/bb123b2f50cdab

https://zenn.dev/petamoriken/articles/f07f48139d9ba1

WebAssembly

WebAssembly JS API の仕様によると WebAssembly.{Compile, Link, Runtime}ErrorNativeError と同様に扱うと定義されています。そして構造化複製アルゴリズムにおいてまだ対応できていません。こちらも AggregateError や Stage 3 Error Cause と同様に対応中です。

https://github.com/whatwg/html/pull/5749

結び

今回は HTML Standard の structuredClone 函数とそれに関連した実装、仕様についての話題を取り上げてみました。便利なので早くブラウザでも扱えるようになって欲しいところです。

仕様追うのは割と楽しいので皆さんもよかったらどうぞ。

脚注
  1. 正確には HTML Standard ではもう構造化複製アルゴリズムとして定義されてはいないようですが、この記事では MDN にあわせてこう記述しています。 ↩︎

  2. RegExp インスタンスの持つ lastIndex プロパティは保持されません。 ↩︎

  3. NativeError ではないエラーオブジェクトは Error へと変換されます。AggregateError については後述。 ↩︎

  4. オブジェクトの持つ Symbol キーでない列挙可能なプロパティのみ保持されます。ArrayRegExp#exec の返り値のように integer-indexed ではないプロパティについても対象となります。 ↩︎

  5. 例外的に IndexedDB では扱えません。 ↩︎

  6. TODO コメントが付いているものの、いいのかなこれ……と思わなくもない。 ↩︎

  7. 変換の際に errors プロパティも取り除かれてしまうため、実質的に何のエラーなのかわからなくなってしまいます。 ↩︎

  8. 余談ですが Temporal は IETF でタイムゾーンとカレンダーの文字フォーマットの承認待ちをしている状況で、12月以降に Stage 4 になると思われます。 ↩︎

Discussion