JavaScript オブジェクトのコピー戦略:浅いコピーと深いコピーの実装と使い分け

参照型の挙動とコピーの必要性

JavaScript におけるオブジェクトは参照型として扱われるため、単純な代入操作では新しいメモリ領域が確保されず、既存のオブジェクトへの参照アドレスがコピーされるだけです。この挙動を理解せずにコピー処理を行うと、予期せぬ副作用が発生する可能性があります。本稿では、ES6 以降の標準機能を用いたオブジェクト複製の手法と、それぞれの適用場面について技術的な観点から解説します。

変数にオブジェクトを代入する際、実際にはオブジェクトの実体ではなく、ヒープメモリ上のアドレスがコピーされます。

const sourceObj = { id: 1001, status: "active" };
const referenceCopy = sourceObj;

referenceCopy.status = "inactive";
console.log(sourceObj.status); // "inactive" が出力され、元データが変更される

このため、元データを保持したまま別の変数で操作したい場合、明示的なコピー処理が必要になります。

浅いコピー(Shallow Copy)の実装方法

浅いコピーは、オブジェクトの最上位プロパティのみを複製し、ネストされたオブジェクトについては参照をコピーします。

Object.assign を利用した合併

Object.assign は複数のソースオブジェクトをターゲットに合併させる際に利用できます。

const defaultConfig = { timeout: 5000, retry: 3 };
const userConfig = { timeout: 10000 };

const mergedConfig = Object.assign({}, defaultConfig, userConfig);
mergedConfig.timeout = 20000;
console.log(defaultConfig.timeout); // 5000 のまま変更されない

ただし、プロパティ値がオブジェクトである場合、その内部状態は共有されます。

展開構文(Spread Syntax)による記述

ES6 の展開構文を使用すると、より簡潔にオブジェクトを複製できます。

const originalData = { user: "admin", permissions: ["read", "write"] };
const shallowClone = { ...originalData };

shallowClone.permissions.push("delete");
console.log(originalData.permissions); // 配列も参照コピーされるため変更が反映される

この手法は、React の状態管理など、イミュータブルな更新が必要な場面でも頻繁に利用されます。

配列の複製における slice

配列に対しては、slice メソッドを用いて浅いコピーを作成できます。

const sourceArray = [1, { value: 10 }, 3];
const copiedArray = sourceArray.slice();

copiedArray[0] = 99;
copiedArray[1].value = 999;
// sourceArray[0] は 1 のまま、sourceArray[1].value は 999 に変更される

深いコピー(Deep Copy)の解決策

深いコピーは、ネストされたすべてのプロパティを再帰的に複製し、完全に独立したオブジェクトを生成します。

JSON シリアライゼーションの活用

最も簡易な方法は、JSON 形式への変換を経由することです。

const complexData = {
  meta: { created: 2023 },
  tags: ["js", "es6"]
};
const deepCopied = JSON.parse(JSON.stringify(complexData));

deepCopied.meta.created = 2024;
console.log(complexData.meta.created); // 2023 のまま

この手法は関数や undefined、Symbol などを処理できないため、純粋なデータ構造に限定されます。

再帰関数による自作実装

特殊な型を扱う場合は、型判定を行った再帰関数が必要です。

function createDeepClone(target) {
  if (target === null || typeof target !== "object") return target;
  if (target instanceof Date) return new Date(target.getTime());
  
  const result = Array.isArray(target) ? [] : {};
  
  for (const key in target) {
    if (target.hasOwnProperty(key)) {
      result[key] = createDeepClone(target[key]);
    }
  }
  return result;
}

循環参照がある場合、このままではスタックオーバーフローを起こすため、WeakMap などで参照履歴を管理する拡張が必要です。

現代のブラウザ機能 structuredClone

近年の環境では、組み込み関数 structuredClone が利用可能です。

const original = { buffer: new ArrayBuffer(10) };
const cloned = structuredClone(original);

これは多くの組み込み型をサポートしており、パフォーマンス面でも最適化されています。

実装選択の基準とパフォーマンス

コピー手法の選定は、データ構造の複雑さとパフォーマンス要件に依存します。

  • 浅いコピーが適しているケース: フラットな構造を持つ設定オブジェクト、プロパティ追加や更新のみを行う場合、処理速度が重要なループ内操作
  • 深いコピーが必須なケース: 状態管理ストアにおけるスナップショット保存、外部 API から取得した複雑なデータ構造の編集、副作用を完全に排除したい純粋関数の実装

開発におけるベストプラクティス

  1. ネイティブ機能の優先: 可能な限り Object.assign や展開構文など、言語標準の機能を利用する。
  2. データ構造の検証: 複製前にオブジェクトに関数や特殊な型が含まれていないか確認する。
  3. ライブラリの活用: 複雑な要件がある場合は、Lodash の cloneDeep などの信頼されたライブラリを採用する。
  4. 不変性データの検討: 頻繁なコピーが必要な場合、Immer などの不変性ライブラリの導入を検討し、ボイラープレートを減らす。
  5. 循環参照への対策: 自作する場合は、必ず循環参照検出のロジックを実装するか、structuredClone の利用を検討する。

タグ: javascript ES6 object-copy deep-clone shallow-clone

5月28日 05:39 投稿