JavaScript | 配列・オブジェクト:ネスト構造の扱い – 深いコピーの注意点

JavaScript JavaScript
スポンサーリンク

深いコピーとは何か

深いコピーは「オブジェクトや配列の入れ子を“最深部まで”複製し、元データと完全に独立させる」操作です。ここが重要です:浅いコピー(スプレッドや Object.assign)は外側だけ新しく、内側は参照を共有します。深いコピーは安全ですが重く、型対応や属性の保持に注意が必要です。

const orig = { user: { name: "Alice" }, tags: ["js"] };

// 浅いコピー(参照共有)
const shallow = { ...orig };
shallow.user.name = "Bob";
console.log(orig.user.name); // "Bob"(共有されていた)

// 深いコピー(完全独立)
const deep = structuredClone(orig);
deep.user.name = "Carol";
console.log(orig.user.name); // "Alice"
JavaScript

代表手段の違い(structuredClone / JSON 経由 / 自作再帰)

structuredClone の特徴と注意点

  • 多くの型(Date, Map, Set, TypedArray, ArrayBuffer 等)を正しく複製し、循環参照も対応します。実務で“最も安全”な選択肢です。
  • 関数、DOMノード、プロトタイプを持つ独自クラスなど、対応外や複製後に“振る舞い”が失われるものがあります。
const a = new Map([["x", { n: 1 }]]);
const b = structuredClone(a);
b.get("x").n = 2;
console.log(a.get("x").n); // 1(独立)
JavaScript

JSON.parse(JSON.stringify(…)) の制限(ここが重要)

  • 関数・undefined・Symbol は落ちる(削除/無視)。
  • Date は文字列化されるだけ。復元には自前の処理が必要。
  • Map/Set/RegExp/TypedArray/BigInt/循環参照は扱えない(例外や消失)。
  • “純粋データ”だけを深いコピーしたい場合に限定して使う。
const orig = { d: new Date(), u: undefined, f: () => {} };
const copy = JSON.parse(JSON.stringify(orig));
console.log(copy.d instanceof Date); // false(文字列化されただけ)
console.log("u" in copy);            // false(削除された)
JavaScript

自作再帰の落とし穴

  • 型の網羅が難しく、属性やプロトタイプ、アクセサ(get/set)が失われやすい。
  • 実務では structuredClone を優先し、どうしても必要な特殊型のみ自前対応に絞るのが現実的。
function deepCopy(x) {
  if (x === null || typeof x !== "object") return x;
  if (Array.isArray(x)) return x.map(deepCopy);
  const out = {};
  for (const [k, v] of Object.entries(x)) out[k] = deepCopy(v);
  return out;
}
JavaScript

属性・プロトタイプ・アクセサの扱い(重要ポイントの深掘り)

値コピーと属性消失の理解

  • スプレッドや assign、JSON 経由は「値のみ」コピーします。writable/configurable/enumerable、get/set といったプロパティ属性は保持されません。
  • クラスインスタンスを“素のオブジェクト”にしてしまうことがあり、メソッドやプロトタイプの振る舞いが失われます。
const src = { 
  get price() { return 100; }, 
  set price(v) {}
};
const copy = { ...src };
typeof Object.getOwnPropertyDescriptor(copy, "price").get; // "undefined"(値だけになった)
JavaScript

独自クラスとプロトタイプ

  • structuredClone は一部のビルトイン型に限って“型らしさ”を保ちますが、独自クラスは基本的にデータだけが複製され、メソッドは再現されません。
  • インスタンスのまま振る舞いを保ちたいなら、クラス側に clone メソッドを用意するのが確実です。
class User {
  constructor(name){ this.name = name; }
  clone(){ return new User(this.name); }
}
const u1 = new User("Alice");
const u2 = u1.clone(); // 振る舞いを保ったコピー
JavaScript

パフォーマンスとメモリ(必要十分のコピーにする)

常に“必要な階層だけ”再構築

  • 毎回全体を深いコピーすると、巨大データでコストが爆発します。
  • 実務では更新したい階層だけをスプレッドで再構築(部分的な深掘り)し、その他は参照を保つのが最適。
const state = { prefs: { theme: "light" }, user: { name: "A" } };
// テーマだけ更新(必要階層のみコピー)
const next = { 
  ...state, 
  prefs: { ...state.prefs, theme: "dark" } 
};
JavaScript

共有参照と同一性の影響

  • 深いコピーは「全ての参照が変わる」ため、差分検知(===)は容易になりますが、キャッシュや同一インスタンスを前提にするロジックが無効化されることに注意。
  • “全部深コピー”を常に選ばず、変更点最小の非破壊更新を基本にする。

循環参照・特殊型・非シリアライズ可能な値

循環参照の扱い

  • JSON 経由は TypeError(循環を展開できない)。
  • structuredClone は循環対応。自作再帰では訪問済みセット(WeakMap)などでループ検出が必要。
const a = {}; a.self = a;
// JSON.stringify(a); // TypeError
const copy = structuredClone(a); // OK
JavaScript

特殊型の注意

  • Date:JSON 経由では文字列化される。復元は reviver や new Date(value) が必要。
  • RegExp/Map/Set:JSON 経由で失われる。structuredClone を使う。
  • DOMノード・関数:深いコピー対象として扱えない(設計上、別の表現を検討)。

実践レシピ(安全な深い更新とコピーの選び方)

“深いコピー”ではなく“部分的な深掘り更新”を優先

function setCity(state, city) {
  return {
    ...state,
    user: { 
      ...state.user, 
      profile: { ...state.user.profile, city } 
    }
  };
}
JavaScript

変更箇所だけ再構築することで、性能と安全性のバランスが取れます。

どうしても全体の独立が必要なら structuredClone

const snapshot = structuredClone(state); // 履歴保存・完全分離が目的のとき
JavaScript

JSON 経由は“純粋データだけ”に限定

const pure = { a: 1, b: "x", c: [2, 3], d: { e: 4 } };
const deep = JSON.parse(JSON.stringify(pure)); // 型制限に注意
JavaScript

まとめ

深いコピーは「完全独立」を得る強力な手段ですが、型の損失・属性の消失・性能コストという注意点が伴います。ここが重要です:基本は“必要な階層だけ再構築する非破壊更新”を優先し、全体独立が必要な場合に限って structuredClone を使う。JSON 経由は純粋データのみ、独自クラスやアクセサは振る舞いが失われる前提で設計する。循環参照や特殊型の取扱いを理解し、同一性とパフォーマンスのバランスを取りながら、安全で意図どおりのコピー戦略を選べば、初心者でも複雑なネスト構造を賢く扱えます。

タイトルとURLをコピーしました