JavaScriptの structuredClone でディープコピーの悩みを終わらせる
JavaScriptでオブジェクトをディープコピーしたいとき、どうしていますか。
JSON.parse(JSON.stringify(obj)) を使っている人、結構いると思います。私もそうでした。でもこれ、Date が文字列になる、undefined が消える、Map や Set が壊れる...という問題があって、正直ちゃんと動いているか不安になる場面があります。
そこに structuredClone という関数が登場しました。Node.js 17以降、ブラウザは2022年以降の主要ブラウザで使えます。知っておくと、ディープコピー周りの悩みがかなり減ります。
この記事で学べること:
-
structuredCloneが何をするものか -
JSON.parse(JSON.stringify)との違い(何が壊れなくなるか) - 注意点と使えないケース
- 実践的な使い方パターン
検証環境: Node.js 18+、主要ブラウザ(Chrome/Firefox/Safari 2022年以降)
まず「ディープコピー」の問題を整理する
ネストしたオブジェクトを扱うとき、こういう問題に出くわしたことがあると思います。
const original = { name: "Alice", address: { city: "Tokyo" } };
const shallow = { ...original }; // スプレッド演算子(シャローコピー)
shallow.address.city = "Osaka";
console.log(original.address.city); // "Osaka" ← 元のオブジェクトも変わってしまう
スプレッド演算子や Object.assign は シャローコピー(浅いコピー)なので、ネストしたオブジェクトは参照がそのまま共有されます。これが意図せぬ副作用の原因になります。
ディープコピー(完全な複製)が必要な場面は多くて、状態管理・イミュータブルな処理・テストデータの準備などで頻繁に出てきます。
これまでのやり方と問題点
JSON.parse(JSON.stringify(obj))
const original = {
name: "Alice",
createdAt: new Date("2026-01-01"),
score: undefined,
tags: new Set(["a", "b"]),
};
const cloned = JSON.parse(JSON.stringify(original));
console.log(cloned.createdAt); // "2026-01-01T00:00:00.000Z" ← Date が文字列になる
console.log(cloned.score); // undefined が消える(プロパティごとなくなる)
console.log(cloned.tags); // {} ← Set が空オブジェクトになる
JSON.stringify は JSON で表現できるものしか正しく変換できません。Date・undefined・Map・Set・RegExp・循環参照などは軒並み壊れます。
lodash の _.cloneDeep
import _ from "lodash";
const cloned = _.cloneDeep(original);
これは動きます。ただ、ディープコピーのためだけに lodash を入れるのはやや重いですし、バンドルサイズを気にする場合は引っかかります。
structuredClone を使う
const original = {
name: "Alice",
createdAt: new Date("2026-01-01"),
score: undefined,
tags: new Set(["a", "b"]),
metadata: new Map([["key", "value"]]),
};
const cloned = structuredClone(original);
console.log(cloned.createdAt instanceof Date); // true ← Date のまま
console.log(cloned.score); // undefined ← 保持される
console.log(cloned.tags instanceof Set); // true ← Set のまま
console.log(cloned.metadata instanceof Map); // true ← Map のまま
// 独立したコピーになっている
cloned.createdAt.setFullYear(2025);
console.log(original.createdAt.getFullYear()); // 2026 ← 元は変わらない
Date、Set、Map、ArrayBuffer、RegExp、undefined、循環参照...これらが全部正しく扱われます。
循環参照にも対応
const obj = { name: "Alice" };
obj.self = obj; // 循環参照
// JSON.parse(JSON.stringify(obj)) → エラー: Converting circular structure to JSON
const cloned = structuredClone(obj); // ✅ 正常に動作
console.log(cloned.self === cloned); // true(循環参照も再現される)
JSON.stringify は循環参照でクラッシュしますが、structuredClone は正しくコピーします。
対応している型・していない型
対応している(正しくコピーされる)
| 型 | JSON方式 | structuredClone |
|---|---|---|
Date |
❌ 文字列化 | ✅ |
Set |
❌ {} になる |
✅ |
Map |
❌ {} になる |
✅ |
RegExp |
❌ {} になる |
✅ |
undefined |
❌ 消える | ✅ |
ArrayBuffer |
❌ | ✅ |
Blob |
❌ | ✅ |
| 循環参照 | ❌ エラー | ✅ |
対応していない(注意が必要)
// ❌ 関数はコピーできない
const obj = { fn: () => "hello" };
const cloned = structuredClone(obj);
// TypeError: () => "hello" could not be cloned
// ❌ クラスのインスタンスはプレーンオブジェクトになる
class User {
constructor(name) { this.name = name; }
greet() { return `Hello, ${this.name}`; }
}
const user = new User("Alice");
const cloned = structuredClone(user);
console.log(cloned instanceof User); // false(プレーンオブジェクトになる)
console.log(cloned.greet); // undefined(メソッドが消える)
// ❌ Symbol はコピーできない
const sym = Symbol("key");
const obj2 = { [sym]: "value" };
const cloned2 = structuredClone(obj2);
console.log(cloned2[sym]); // undefined
関数・クラスインスタンスのメソッド・Symbol は structuredClone でコピーできません。これが使えないケースの主な理由です。
実践パターン
パターン1: 状態管理でのイミュータブル更新
React や Vue で状態をイミュータブルに更新したいとき。
// Before: スプレッドでのネスト更新(深いと面倒)
const newState = {
...state,
user: {
...state.user,
address: {
...state.user.address,
city: "Osaka",
},
},
};
// After: structuredClone でシンプルに
const newState = structuredClone(state);
newState.user.address.city = "Osaka";
深いネストの更新がシンプルに書けます。
パターン2: テストデータの準備
const baseUser = {
id: 1,
name: "Alice",
createdAt: new Date("2026-01-01"),
permissions: new Set(["read", "write"]),
};
test("管理者ユーザーの権限テスト", () => {
const adminUser = structuredClone(baseUser);
adminUser.permissions.add("admin");
// baseUser.permissions は変わらない
expect(baseUser.permissions.has("admin")).toBe(false);
expect(adminUser.permissions.has("admin")).toBe(true);
});
テストケースごとにベースデータを安全に複製できます。
パターン3: API レスポンスの加工
async function fetchAndProcess(url) {
const response = await fetch(url);
const data = await response.json();
// 元データを保持しながら加工版を作る
const processed = structuredClone(data);
processed.items = processed.items.map(item => ({
...item,
label: item.name.toUpperCase(),
}));
return { original: data, processed };
}
transferable オプション(ArrayBuffer の高速コピー)
structuredClone には transfer オプションがあって、ArrayBuffer をコピーではなく移動できます。
const buffer = new ArrayBuffer(1024);
const cloned = structuredClone(buffer, { transfer: [buffer] });
// buffer は移動されたので使えなくなる(コピーコストゼロ)
console.log(buffer.byteLength); // 0(detached状態)
console.log(cloned.byteLength); // 1024
大きなバイナリデータを扱うとき、パフォーマンス的に有利です。
使い分けの判断基準
こういう感じで使い分けるのがいいと思います。
structuredClone を使う場面:
- Date / Set / Map / undefined を含むオブジェクトのディープコピー
- 循環参照がある可能性のあるオブジェクト
- lodash を入れたくないが
_.cloneDeep相当が必要
JSON.parse(JSON.stringify) で十分な場面:
- JSON で表現できる純粋なデータ(プリミティブ・配列・プレーンオブジェクトのみ)
- 実行環境が古い(structuredClone が使えない)
別の手段が必要な場面:
- 関数を含むオブジェクト → 手動で再構築するか lodash の
_.cloneDeep - クラスインスタンスをメソッドごとコピー → クラスに
clone()メソッドを実装する
まとめ
structuredClone は 2022年以降の主要ブラウザと Node.js 17+ で使える、組み込みのディープコピー関数です。
// JSON 方式の問題をまとめて解決
const cloned = structuredClone(original);
JSON.parse(JSON.stringify) の代替として、Date・Set・Map・循環参照などを正しく扱ってくれます。外部ライブラリ不要で使えるのが地味に嬉しいところです。
関数やクラスインスタンスのメソッドはコピーできない点だけ覚えておけば、多くのケースで「これでいい」と思います。ディープコピーが必要な場面で JSON.parse(JSON.stringify) が出てきたら、まず structuredClone を試してみる、そういう感覚でいいと思います。