0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

JavaScriptの structuredClone でディープコピーの悩みを終わらせる

0
Posted at

JavaScriptの structuredClone でディープコピーの悩みを終わらせる

JavaScriptでオブジェクトをディープコピーしたいとき、どうしていますか。

JSON.parse(JSON.stringify(obj)) を使っている人、結構いると思います。私もそうでした。でもこれ、Date が文字列になる、undefined が消える、MapSet が壊れる...という問題があって、正直ちゃんと動いているか不安になる場面があります。

そこに 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 で表現できるものしか正しく変換できません。DateundefinedMapSetRegExp・循環参照などは軒並み壊れます。

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 ← 元は変わらない

DateSetMapArrayBufferRegExpundefined、循環参照...これらが全部正しく扱われます。

循環参照にも対応

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

関数・クラスインスタンスのメソッド・SymbolstructuredClone でコピーできません。これが使えないケースの主な理由です。


実践パターン

パターン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 を試してみる、そういう感覚でいいと思います。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?