🧬

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

6 min read

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

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

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

対応している値

2021年10月現在の対応している値について書いています。これらは増える予定です。

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

polyfill

ちょうど core-js に実装されようとしているので、そちらを待つのがよいかと思います。[Transferable] の対応は出来ないためありません。

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

仕様対応の遅れ

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 に追随できていない問題については次回の TC39 会議で議題にあがるようです。

https://github.com/tc39/agendas/blob/master/2021/10.md#agenda-items

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

ログインするとコメントできます