JavaScriptのstructuredClone():入れ子のデータを独立してコピーする
structuredClone()で入れ子のオブジェクトを深くコピーする方法を解説。ブラウザーとNode.jsの対応、実行手順、循環参照、コピーできない値、転送時の注意点をコードで学びます。
編集前のデータを残したいときに使う
設定画面で入力を変更し、保存するまでは元の設定を維持したい。このような場面では、入れ子になったオブジェクトまで独立した編集用データが必要です。
structuredClone()は、対応する値を深くコピーするAPIです。通常のオブジェクトや配列だけでなく、DateやMapなども扱えます。単に別の変数へ代入すると同じオブジェクトを参照しますが、この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"が返ることを確認してください。
typeof globalThis.structuredCloneその後、各サンプルのJavaScriptコードをブロック全体で実行します。出力例はconsole.log()が表示する行です。Consoleが評価結果として追加表示するundefinedは、記事の出力例には含めていません。
Node.jsで実行する
Node.js 22系または24系がインストールされた環境で、コードをclone-demo.jsとして保存します。ターミナルで保存先のフォルダーへ移動し、次を実行してください。
node --version
node clone-demo.js後続の例も、同じファイルの内容を置き換えて実行できます。サンプルはそれぞれ独立しており、前の例の変数を引き継ぐ必要はありません。
引数・戻り値と処理の性質
呼び出し形式はstructuredClone(value, options)です。
valueはコピーする値です。入れ子の中身もコピー可能な値である必要があります。optionsは省略できます。transferに配列を渡すと、指定した転送可能オブジェクトの資源をコピー先へ移します。- 戻り値はコピーされた値です。オブジェクトを渡した場合、元とは別のオブジェクトになります。
- コピーできない値が含まれると、
DataCloneErrorという名前の例外が発生します。
戻り値はPromiseではなく、処理は同期的です。awaitを付けてもコピー処理自体が別スレッドへ移るわけではありません。大量のデータを頻繁にコピーすると、呼び出し元の処理を待たせるため、必要な範囲に絞って使います。
引数、戻り値、例外、転送オプションの定義は、MDNのstructuredClone()リファレンスに記載されています。
自己完結した例:設定の編集用コピーを作る
次のコードは、元の設定をコピーしてから、通知設定、配列、日時を変更します。最後に元とコピーの値を比較します。
(() => {
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}`);
})();対応環境での期待出力は次のとおりです。ここに示す出力は仕様に基づく静的確認であり、実機検証結果ではありません。
元の通知: true
編集用の通知: false
元のタグ: JavaScript
編集用のタグ: JavaScript,Node.js
元の日時: 2026-09-01T00:00:00.000Z
編集用の日時: 2026-09-02T00:00:00.000Z
内側も別物: true
Dateを維持: truedraft.profileもdraft.tagsも元とは別のオブジェクトです。日時も文字列へ変換されず、Dateのまま扱えます。UTCを明示した日時と操作を使っているため、この出力は実行環境のタイムゾーンに依存しません。
循環参照もコピーできる
自分自身を参照するデータでもコピーできます。次の例では、コピー先のselfがコピー先自身を指します。
(() => {
const original = { name: "設定" };
original.self = original;
const copy = structuredClone(original);
console.log(copy !== original);
console.log(copy.self === copy);
console.log(copy.self === original);
})();期待出力は順にtrue、true、falseです。循環を解消する処理を自作する必要はありません。循環参照の対応は、MDNの説明と例でも示されています。
失敗例とコピーの制約
関数を含めると例外になる
設定値と処理用の関数を一緒に持つオブジェクトは、そのままコピーできません。関数を無視して残りだけコピーする動作ではなく、呼び出しが失敗します。
(() => {
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などの処理を分け、データ側だけをコピーする設計にすると扱いやすくなります。
オブジェクトの振る舞いまでは複製しない
通常のオブジェクト、配列、Date、Map、Setなどは対象ですが、関数やDOMノードは対象外です。また、独自クラスのプロトタイプやprivate要素、プロパティの読み取り専用設定などは保持されません。RegExpのlastIndexも維持されません。MDNの構造化クローンアルゴリズムに対応型と制約がまとめられています。
そのため、クラスのインスタンスを渡して「メソッドを持った同じ種類のインスタンスができる」と考えないでください。コピーしたいものがデータなのか、振る舞いを持つインスタンスなのかを先に判断します。
transferを使うと元のバッファが使えなくなる
transferは、転送可能な資源を移すオプションです。編集前のデータを残す用途では省略します。次の例では、Uint8Arrayが持つArrayBufferを転送します。
(() => {
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}`);
})();期待出力は次のとおりです。
転送先: 10,20,30
元のバッファ長: 0転送リストに指定するのは、この例ではsourceではなくsource.bufferです。転送後は元のバッファが切り離されるため、元データを引き続き読む設計には使えません。別のビューが同じバッファを参照している場合、そのビューにも影響します。
転送の意味と元の資源が利用できなくなる点は、MDNの転送に関する説明を参照してください。
活用場面と導入の判断
設定フォームの編集開始時にコピーを作れば、キャンセル時には編集用データを破棄するだけで元の設定を残せます。また、複数の計算条件を試すときに、同じ初期データから独立した入力を作る用途にも使えます。
履歴機能では、変更前のデータをコピーして保持する方法が考えられます。ただし、履歴の数だけメモリーを消費するため、保存件数や対象範囲を決めておく必要があります。
導入時は、まずコピー対象を通常のデータに絞り、関数や独自クラスが混ざっていないか確認します。そのうえで、編集開始など必要なタイミングで呼び出してください。単一の項目だけを更新する処理まで、毎回データ全体を深くコピーする必要はありません。
公式参考資料
Googleの優先するニュース提供元にSEの部屋を追加
Googleで、いつも読みたい情報源を選べます。登録可否はGoogleの画面で確認できます。
候補にSEの部屋が表示されない場合は、まだ追加できません。