JavaScriptの浅いコピーと深いコピーの違いをわかりやすく整理しました。

JavaScriptの浅いコピーと深いコピーの違いをわかりやすく整理しました。

D
dongAuthor
4 min read

JavaScriptで開発していると、オブジェクトをコピーしなければならない状況が頻繁に発生します。でも単純に = 演算子でコピーすると、思いがけないバグを引き起こすことがあります。なぜならオブジェクトは参照型だからです。

この記事では浅いコピーと深いコピーの違い、それぞれの使用ケース、そして実務で今すぐ使える実装方法を解説します。

浅いコピー(Shallow Copy)とは?

浅いコピーはオブジェクトの最上位のプロパティだけをコピーする方式です。コピーされたオブジェクトと元のオブジェクトが 同じ参照を共有するため、ネストされたオブジェクトを修正すると元にも影響が出ます。

浅いコピーの動作原理

浅いコピーを行うと新しいオブジェクトが生成されます(o1 !== o2)が、内部のネストされたオブジェクトや配列は同じメモリアドレスを指します。

const original = {
  name: "홍길동",
  age: 30,
  address: {city: "서울",district: "강남구"
  }
};

const shallowCopy = Object.assign({}, original);

// 最上位プロパティの変更 – 元に影響なし
shallowCopy.name = "김철수";
console.log(original.name); // "홍길동"

// ネストされたオブジェクトの変更 – 元にも影響!
shallowCopy.address.city = "부산";
console.log(original.address.city); // "부산"

浅いコピー実装方法

JavaScriptで浅いコピーを行う方法はいくつかあります:

1. Object.assign()を使用

const user = {
  name: "김민수",
  role: "개발자"
};

const clone = Object.assign({}, user);

2. スプレッド構文(Spread Syntax)を使用

const user = {
  name: "박지영",
  role: "디자이너"
};

const clone = { ...user };

3. 配列の場合

const items = [1, 2, { value: 3 }];

// Array.from()
const copy1 = Array.from(items);

// slice()
const copy2 = items.slice();

// concat()
const copy3 = [].concat(items);

// スプレッド構文
const copy4 = [...items];

浅いコピーの特徴

浅くコピーされたオブジェクトの最上位のプロパティを再代入しても元のオブジェクトには影響を与えません。しかし、ネストされたオブジェクトのプロパティを変更すると元のオブジェクトも一緒に変更されます。

const ingredientsList = ["국수", { list: ["계란", "밀가루", "물"] }];

const ingredientsListCopy = Array.from(ingredientsList);

// 最上位要素の変更
ingredientsListCopy[0] = "쌀국수";
console.log(ingredientsList[0]); // "국수"(影響なし)

// ネストされたオブジェクトの変更
ingredientsListCopy[1].list = ["쌀가루", "물"];
console.log(ingredientsList[1].list); // ["쌀가루", "물"](影響あり!)

深いコピー(Deep Copy)とは?

深いコピーはオブジェクトのすべてのネストされたプロパティまで完全に新しいコピーを作る方式です。コピー本体を修正しても元のオブジェクトには全く影響を与えません。

深いコピーの定義

二つのオブジェクトが深いコピー関係にあるためには、次の条件を満たさなければなりません:

  1. 二つのオブジェクトは別個のオブジェクトであること (o1 !== o2)
  2. プロパティの名前と順序が同じであること
  3. プロパティの値がそれぞれの深いコピーであること
  4. プロトタイプチェーンが構造的に同一であること

深いコピーの動作原理

深いコピーはネストされたすべてのオブジェクトを再帰的にコピーします。したがって、コピー本体のどこを変更しても元には影響を与えません。

const original = {
  name: "이수진",
  projects: {current: ["프로젝트 A", "프로젝트 B"],completed: ["프로젝트 X"]
  }
};

const deepCopy = JSON.parse(JSON.stringify(original));

deepCopy.projects.current.push("프로젝트 C");
console.log(original.projects.current); // ["프로젝트 A", "프로젝트 B"](影響なし)

深いコピー実装方法

1. JSON.parse(JSON.stringify())を活用

もっとも簡単で広く使われる方法です。

const ingredientsList = ["국수", { list: ["계란", "밀가루", "물"] }];
const ingredientsListDeepCopy = JSON.parse(JSON.stringify(ingredientsList));

ingredientsListDeepCopy[1].list = ["쌀가루", "물"];
console.log(ingredientsList[1].list); // ["계란", "밀가루", "물"](影響なし)

長所:

  • 実装が簡単で直感的です
  • 別のライブラリが不要です

短所:

  • 関数はコピーされません
  • Date オブジェクトは文字列に変換されます
  • undefinedSymbol といった特殊値は無視されます
  • 循環参照があるとエラーになります
const obj = {
  date: new Date(),
  func: () => console.log("hello"),
  undef: undefined
};

const copy = JSON.parse(JSON.stringify(obj));
console.log(copy);
// { date: "2024‑01‑15T10:30:00.000Z" }
// funcとundefは消えます

2. structuredClone()の使用

JavaScriptが標準で提供するメソッドです。

const original = {
  name: "최유진",
  date: new Date(),
  nested: {data: [1, 2, 3]
  }
};

const deepCopy = structuredClone(original);

deepCopy.nested.data.push(4);
console.log(original.nested.data); // [1, 2, 3](影響なし)
console.log(deepCopy.date instanceof Date); // true

長所:

  • Date, RegExp, Map, Set など多様な組み込みオブジェクトを正確にコピーします
  • 循環参照も扱えます
  • パフォーマンスが優れています

短所:

  • 関数はやはりコピーされません
  • 古いブラウザではサポートされていない可能性があります

3. 再帰関数を使ったカスタム実装

完全な制御が必要な場合は自分で実装できます。

function deepClone(obj, hash = new WeakMap()) {
  // nullまたは原始型ならそのまま返す
  if (obj === null || typeof obj !== 'object') {return obj;
  }
  
  // 循環参照処理
  if (hash.has(obj)) {return hash.get(obj);
  }
  
  // Date, RegExp 等の特別なオブジェクト処理
  if (obj instanceof Date) return new Date(obj);
  if (obj instanceof RegExp) return new RegExp(obj);
  
  // 新しいオブジェクト作成
  const cloneObj = Array.isArray(obj) ? [] : {};
  hash.set(obj, cloneObj);
  
  // 再帰的にコピー
  for (let key in obj) {if (obj.hasOwnProperty(key)) {  cloneObj[key] = deepClone(obj[key], hash);}
  }
  
  return cloneObj;
}

const original = {
  name: "정민호",
  skills: ["JavaScript", "React"],
  experience: {years: 5,companies: ["A사", "B사"]
  }
};

const copy = deepClone(original);
copy.skills.push("TypeScript");
console.log(original.skills); // ["JavaScript", "React"](影響なし)

長所:

  • 必要に応じてカスタマイズ可能です
  • 特殊な型も望む形で対応できます

短所:

  • 実装と保守が複雑です
  • 全てのエッジケースを考慮する必要があります

4. Lodashライブラリ活用

Lodash は実務でよく使われるJavaScriptユーティリティライブラリです。

import _ from 'lodash';

const original = {
  name: "강서연",
  metadata: {tags: ["frontend", "react"],createdAt: new Date()
  }
};

const deepCopy = _.cloneDeep(original);

長所:

  • 検証済みの実装で安定しています
  • 多くのエッジケースを処理します

短所:

  • 追加のライブラリをインストールする必要があります
  • バンドルサイズが増える可能性があります(もちろん lodash の容量が巨大というわけではありませんが…)

いつ浅いコピーを使うか?

浅いコピーは次のような状況に適しています:

1. パフォーマンスが重要な場合

ネストされたオブジェクトを個別に修正する必要がなければ、浅いコピーのほうが速くてメモリ効率も高いです。

const userConfig = {
  theme: "dark",
  language: "ko"
};

// 最上位プロパティだけ変更するので浅いコピーで十分
const newConfig = { ...userConfig, theme: "light" };

2. 最上位プロパティだけ変更する場合

ネストされたオブジェクトを触らないなら浅いコピーだけでも十分です。

const product = {
  id: 1,
  name: "노트북",
  price: 1500000
};

const updatedProduct = { ...product, price: 1400000 };

3. ReactでのProps伝達時

Reactでは不変性を保つために浅いコピーを頻繁に使います。

const handleUpdate = (newData) => {
  setUser({ ...user, ...newData });
};

いつ深いコピーを使うか?

深いコピーは次のような状況で必要です:

1. 完全に独立したコピーが必要な場合

元とまったく分離されたオブジェクトが必要なときに、深いコピーを使います。

const template = {
  title: "기본 템플릿",
  sections: [{ type: "header", content: "제목" },{ type: "body", content: "본문" }
  ]
};

// 各ユーザーごとに独立したテンプレートが必要
const userTemplate = structuredClone(template);
userTemplate.sections[0].content = "사용자 제목";

2. ネストされたオブジェクトを修正しなければならない場合

複雑なデータ構造を扱う際には深いコピーが必要です。

const projectData = {
  name: "신규 프로젝트",
  team: {frontend: ["김개발", "이디자인"],backend: ["박서버", "최디비"]
  }
};

const archivedProject = structuredClone(projectData);
archivedProject.team.frontend.push("신입사원");
// originalのteamには影響なし

3. 状態履歴を管理する場合

「実行取り消し(Undo)」機能を実装する際に有用です。

const history = [];

function saveState(currentState) {
  history.push(structuredClone(currentState));
}

function undo() {
  return history.pop();
}

パフォーマンス考慮事項

深いコピーと浅いコピーではパフォーマンスに差があります。

浅いコピーのパフォーマンス

浅いコピーは最上位プロパティだけをコピーするため非常に速いです。オブジェクトのサイズが大きいほど、深いコピーに比べてメリットが大きくなります。

// 速い
const shallowCopy = { ...largeObject };

深いコピーのパフォーマンス

深いコピーはすべてのネスト構造を巡回するため、当然ながら処理時間が長くなります。

// 比較的遅い
const deepCopy = structuredClone(largeObject);

パフォーマンス最適化のヒント

大規模なオブジェクトを扱うときは次を検討してください:

  1. 必要な部分だけコピーする
const { metadata, ...essentialData } = largeObject;
const copy = structuredClone(essentialData);
  1. メモ化(Memoization)を活用
const cache = new WeakMap();

function getCachedCopy(obj) {
  if (!cache.has(obj)) {cache.set(obj, structuredClone(obj));
  }
  return cache.get(obj);
}
  1. 適切な方法を選択する
  • シンプルなオブジェクト:JSON.parse(JSON.stringify())
  • 複雑なオブジェクト:structuredClone()
  • カスタムロジックが必要:自分で実装するかまたは Lodash

よくある質問

Q: 配列も同じ方式でコピーするのですか?

はい、配列もオブジェクトなので同じ原理が適用されます。浅いコピーならスプレッド構文や slice() を、深いコピーなら structuredClone() を使うと良いです。

Q: Reactではどのコピー方式が主に使われていますか?

Reactではほとんどの場合、浅いコピーが使用されます。状態更新時に { ...state } のように新しいオブジェクトを作って不変性を保ちます。

Q: JSON方式の深いコピーで十分ではないのですか?

単純なデータ構造には十分ですが、Date、関数、undefined などを扱う必要があるなら structuredClone() や Lodash を使うほうが良い方法です!

References