JavaScript

JavaScriptのstructuredClone():入れ子のデータを独立してコピーする

この記事でわかること

structuredClone()で入れ子のオブジェクトを深くコピーする方法を解説。ブラウザーとNode.jsの対応、実行手順、循環参照、コピーできない値、転送時の注意点をコードで学びます。

編集前のデータを残したいときに使う

設定画面で入力を変更し、保存するまでは元の設定を維持したい。このような場面では、入れ子になったオブジェクトまで独立した編集用データが必要です。

structuredClone()は、対応する値を深くコピーするAPIです。通常のオブジェクトや配列だけでなく、DateMapなども扱えます。単に別の変数へ代入すると同じオブジェクトを参照しますが、このAPIなら対応するデータ構造を複製できます。

オブジェクトのスプレッド構文によるコピーは浅く、内側のオブジェクトの参照を共有します。深いコピーとの違いは、web.devの解説でも確認できます。本記事では、編集用データの作成を軸に使い方を学びます。

動作環境と対応バージョン

2026年9月23日時点で確認した公式資料では、ブラウザーの基本対応はChrome 98、Edge 98、Firefox 94、Safari 15.4以降です。web.devの対応情報に各ブラウザーの導入バージョンが掲載されています。

Node.jsにもグローバル関数として用意されており、v17.0.0で追加されました。本記事のコードはNode.js 22系・24系でも利用でき、追加パッケージやimportは不要です。導入バージョンは、Node.jsのグローバルAPI資料で確認できます。

以下の例はブラウザーとNode.jsの両方で使える値だけを扱います。ブラウザー専用のオブジェクトまで、Node.jsで同じように扱えるという意味ではありません。

ブラウザーで実行する

対応ブラウザーで空のタブを開き、開発者ツールのConsoleを選びます。次の式を入力して、"function"が返ることを確認してください。

JavaScript
typeof globalThis.structuredClone

その後、各サンプルのJavaScriptコードをブロック全体で実行します。出力例はconsole.log()が表示する行です。Consoleが評価結果として追加表示するundefinedは、記事の出力例には含めていません。

Node.jsで実行する

Node.js 22系または24系がインストールされた環境で、コードをclone-demo.jsとして保存します。ターミナルで保存先のフォルダーへ移動し、次を実行してください。

Shell
node --version
node clone-demo.js

後続の例も、同じファイルの内容を置き換えて実行できます。サンプルはそれぞれ独立しており、前の例の変数を引き継ぐ必要はありません。

引数・戻り値と処理の性質

呼び出し形式はstructuredClone(value, options)です。

  • valueはコピーする値です。入れ子の中身もコピー可能な値である必要があります。
  • optionsは省略できます。transferに配列を渡すと、指定した転送可能オブジェクトの資源をコピー先へ移します。
  • 戻り値はコピーされた値です。オブジェクトを渡した場合、元とは別のオブジェクトになります。
  • コピーできない値が含まれると、DataCloneErrorという名前の例外が発生します。

戻り値はPromiseではなく、処理は同期的です。awaitを付けてもコピー処理自体が別スレッドへ移るわけではありません。大量のデータを頻繁にコピーすると、呼び出し元の処理を待たせるため、必要な範囲に絞って使います。

引数、戻り値、例外、転送オプションの定義は、MDNのstructuredClone()リファレンスに記載されています。

自己完結した例:設定の編集用コピーを作る

次のコードは、元の設定をコピーしてから、通知設定、配列、日時を変更します。最後に元とコピーの値を比較します。

JavaScript
(() => {
  if (typeof globalThis.structuredClone !== "function") {
    console.log("structuredClone()に対応した環境が必要です");
    return;
  }

  const original = {
    profile: { name: "Aki", notifications: true },
    tags: ["JavaScript"],
    updatedAt: new Date("2026-09-01T00:00:00.000Z")
  };

  const draft = structuredClone(original);
  draft.profile.notifications = false;
  draft.tags.push("Node.js");
  draft.updatedAt.setUTCDate(2);

  console.log(`元の通知: ${original.profile.notifications}`);
  console.log(`編集用の通知: ${draft.profile.notifications}`);
  console.log(`元のタグ: ${original.tags.join(",")}`);
  console.log(`編集用のタグ: ${draft.tags.join(",")}`);
  console.log(`元の日時: ${original.updatedAt.toISOString()}`);
  console.log(`編集用の日時: ${draft.updatedAt.toISOString()}`);
  console.log(`内側も別物: ${original.profile !== draft.profile}`);
  console.log(`Dateを維持: ${draft.updatedAt instanceof Date}`);
})();

対応環境での期待出力は次のとおりです。ここに示す出力は仕様に基づく静的確認であり、実機検証結果ではありません。

CODE
元の通知: true
編集用の通知: false
元のタグ: JavaScript
編集用のタグ: JavaScript,Node.js
元の日時: 2026-09-01T00:00:00.000Z
編集用の日時: 2026-09-02T00:00:00.000Z
内側も別物: true
Dateを維持: true

draft.profiledraft.tagsも元とは別のオブジェクトです。日時も文字列へ変換されず、Dateのまま扱えます。UTCを明示した日時と操作を使っているため、この出力は実行環境のタイムゾーンに依存しません。

循環参照もコピーできる

自分自身を参照するデータでもコピーできます。次の例では、コピー先のselfがコピー先自身を指します。

JavaScript
(() => {
  const original = { name: "設定" };
  original.self = original;

  const copy = structuredClone(original);
  console.log(copy !== original);
  console.log(copy.self === copy);
  console.log(copy.self === original);
})();

期待出力は順にtruetruefalseです。循環を解消する処理を自作する必要はありません。循環参照の対応は、MDNの説明と例でも示されています。

失敗例とコピーの制約

関数を含めると例外になる

設定値と処理用の関数を一緒に持つオブジェクトは、そのままコピーできません。関数を無視して残りだけコピーする動作ではなく、呼び出しが失敗します。

JavaScript
(() => {
  const settings = {
    theme: "dark",
    onSave: () => "保存"
  };

  try {
    structuredClone(settings);
    console.log("コピー成功");
  } catch (error) {
    if (error?.name !== "DataCloneError") {
      throw error;
    }
    console.log("コピー失敗: DataCloneError");
  }
})();

期待出力はコピー失敗: DataCloneErrorです。同期例外なので、呼び出しを囲むtry...catchで捕捉します。想定外の例外は再送出して、原因を隠さないようにしています。

実務では、themeなどのデータとonSaveなどの処理を分け、データ側だけをコピーする設計にすると扱いやすくなります。

オブジェクトの振る舞いまでは複製しない

通常のオブジェクト、配列、DateMapSetなどは対象ですが、関数やDOMノードは対象外です。また、独自クラスのプロトタイプやprivate要素、プロパティの読み取り専用設定などは保持されません。RegExplastIndexも維持されません。MDNの構造化クローンアルゴリズムに対応型と制約がまとめられています。

そのため、クラスのインスタンスを渡して「メソッドを持った同じ種類のインスタンスができる」と考えないでください。コピーしたいものがデータなのか、振る舞いを持つインスタンスなのかを先に判断します。

transferを使うと元のバッファが使えなくなる

transferは、転送可能な資源を移すオプションです。編集前のデータを残す用途では省略します。次の例では、Uint8Arrayが持つArrayBufferを転送します。

JavaScript
(() => {
  const source = new Uint8Array([10, 20, 30]);
  const moved = structuredClone(source, {
    transfer: [source.buffer]
  });

  console.log(`転送先: ${Array.from(moved).join(",")}`);
  console.log(`元のバッファ長: ${source.buffer.byteLength}`);
})();

期待出力は次のとおりです。

CODE
転送先: 10,20,30
元のバッファ長: 0

転送リストに指定するのは、この例ではsourceではなくsource.bufferです。転送後は元のバッファが切り離されるため、元データを引き続き読む設計には使えません。別のビューが同じバッファを参照している場合、そのビューにも影響します。

転送の意味と元の資源が利用できなくなる点は、MDNの転送に関する説明を参照してください。

活用場面と導入の判断

設定フォームの編集開始時にコピーを作れば、キャンセル時には編集用データを破棄するだけで元の設定を残せます。また、複数の計算条件を試すときに、同じ初期データから独立した入力を作る用途にも使えます。

履歴機能では、変更前のデータをコピーして保持する方法が考えられます。ただし、履歴の数だけメモリーを消費するため、保存件数や対象範囲を決めておく必要があります。

導入時は、まずコピー対象を通常のデータに絞り、関数や独自クラスが混ざっていないか確認します。そのうえで、編集開始など必要なタイミングで呼び出してください。単一の項目だけを更新する処理まで、毎回データ全体を深くコピーする必要はありません。

公式参考資料

PREFERRED SOURCES

Googleの優先するニュース提供元にSEの部屋を追加

Googleで、いつも読みたい情報源を選べます。登録可否はGoogleの画面で確認できます。

Googleの設定画面で確認する 新しいタブで開きます

候補にSEの部屋が表示されない場合は、まだ追加できません。

y.
WRITTEN BY

y_ymo10

SEの部屋で、HTML・CSSからJavaScript・React・Next.jsまで、Web制作の開発ノートを公開しています。

ほかの開発ノートを読む →