記事内にはプロモーションが含まれています

【JavaScript】オブジェクトをコピーする正しい方法

【JavaScript】オブジェクトをコピーする正しい方法 JavaScript

JavaScriptで開発をしていると、オブジェクトや配列を別の変数にコピーして使いたい場面が頻繁に訪れます。

しかし、「コピーしたはずのデータを変更したら、元のデータまで変わってしまった!」というバグに遭遇したことはないでしょうか?

これは、JavaScriptのオブジェクトが「値」そのものではなく「参照(メモリ上の場所)」で扱われるという特性によるものです。

この仕組みを理解せずに単純な代入を行ってしまうと、予期せぬ不具合の原因となります。

この記事では、JavaScriptにおけるオブジェクトコピーの落とし穴である「参照の共有」の解説から、スプレッド構文を使った手軽なコピー、そして最新の標準機能である structuredClone を使った完全なコピー(ディープコピー)まで、状況に応じた正しいコピー方法を徹底解説します。

【本記事の信頼性】
プロフィール
執筆者:マヒロ
  • 執筆者は元エンジニア
  • SES⇒大手の社内SE⇒独立
  • 現在はこじんまりとしたプログラミングスクールを運営
  • モットーは「利他の精神」

JavaScriptにおけるオブジェクトコピーの基本と落とし穴

JavaScriptでオブジェクトや配列を扱う際、最も注意しなければならないのが「コピーの深さ」と「参照」の概念です。

まずは、なぜ単純な代入ではうまくいかないのか、そして「シャローコピー」と「ディープコピー」の違いについて理解しましょう。

単なる代入は「コピー」ではない(参照のコピー)

変数を別の変数に = で代入しただけでは、オブジェクトの中身はコピーされません。
コピーされるのは、オブジェクトがメモリ上のどこにあるかを示す「住所(参照)」だけです。

厳密にはJavaScriptはすべて「値渡し」ですが、オブジェクトの場合はその「値」が参照であるため、結果として参照がコピーされます(参照の値渡し)。

以下のコードを見てみましょう。

const original = { name: "Tanaka", age: 25 };

// 変数を代入(参照のコピー)
const copy = original;

// コピー先のプロパティを変更
copy.age = 30;

console.log("copy:", copy);
console.log("original:", original);

実行結果

copy: { name: 'Tanaka', age: 30 }
original: { name: 'Tanaka', age: 30 }

const copy = original; とした時点で、この2つの変数は同じオブジェクト(同じメモリ上の場所)を指し示すようになります。

そのため、copy の内容を変更すると、同じ場所を見ている original の内容も変わってしまいます。

これを防ぐためには、新しいオブジェクトを作成して中身を移す「複製」の処理が必要です。

シャローコピー(浅いコピー)とディープコピー(深いコピー)の違い

オブジェクトを複製する方法には、大きく分けて2つのレベルがあります。

シャローコピー(浅いコピー)
オブジェクトの「1階層目」だけを新しい入れ物にコピーします。
ただし、オブジェクトの中にさらにオブジェクト(ネスト)がある場合、その内側のオブジェクトは「参照」のままコピーされます。

ディープコピー(深いコピー)
オブジェクトの階層がどれだけ深くても、すべてを再帰的に複製します。
コピー元とコピー先は完全に切り離され、互いに影響を与えることはありません。

シャローコピー(浅いコピー)の実装方法

ネスト(入れ子)されていないシンプルなオブジェクトであれば、処理が高速なシャローコピーで十分です。

現代のJavaScript開発では、主に「スプレッド構文」が使用されます。

スプレッド構文 ... を使う(推奨)

ES2015(ES6)から導入されたスプレッド構文を使うと、非常に簡潔にオブジェクトをコピーできます。

... を使ってオブジェクトの中身を展開し、新しい {} の中に詰め込むイメージです。

const original = { name: "Suzuki", age: 28 };

// スプレッド構文でシャローコピー
const copy = { ...original };

// コピー先を変更
copy.age = 35;

console.log("copy:", copy);
console.log("original:", original);

実行結果

copy: { name: 'Suzuki', age: 35 }
original: { name: 'Suzuki', age: 28 }

{ ...original } と記述することで、original のプロパティを展開して新しいオブジェクトを作成しています。

今度は copy を変更しても、original は影響を受けていません。

シンプルなデータの複製なら、この方法が最も一般的で推奨されます。

Object.assign() を使う

スプレッド構文が登場する前によく使われていた方法です。
現在でも互換性維持などのコードで見かけることがあります。

const original = { name: "Sato", isStaff: true };

// Object.assignで空のオブジェクトにプロパティをコピー
const copy = Object.assign({}, original);

console.log(copy);

実行結果

{ name: 'Sato', isStaff: true }

Object.assign({}, original) は、第一引数の空オブジェクト {} に対して、第二引数 original のプロパティを上書きコピーして返します。

結果として新しいオブジェクトが生成されます。

シャローコピーの限界(ネストされたデータの罠)

シャローコピーはあくまで「浅い」コピーです。

オブジェクトの中にさらにオブジェクトがある場合(ネスト構造)、内側のデータは参照共有されたままになります。

const original = {
  id: 1,
  details: { color: "red", size: "L" } // ネストされたオブジェクト
};

const shallowCopy = { ...original };

// ネストされた中のデータを変更
shallowCopy.details.color = "blue";

console.log(original.details.color);

実行結果

blue

shallowCopy の中の details を変更したはずなのに、originaldetails.color"blue" に変わってしまいました。

これは、1階層目の iddetails というプロパティ自体はコピーされましたが、details の中身(オブジェクトへの参照)はそのままコピーされたためです。

このような複雑なデータを扱う場合は、次項の「ディープコピー」が必要になります。

ディープコピー(深いコピー)の実装方法

ネストされたオブジェクトや配列を含む複雑なデータを完全に複製するには、ディープコピーを行います。

以前は外部ライブラリやJSON変換を使うのが定石でしたが、現在は標準機能だけで安全に実現できるようになりました。

structuredClone() を使う(推奨・モダン)

2022年頃から主要なブラウザやNode.jsでサポートされた structuredClone() 関数は、JavaScript標準のディープコピー機能です。

特別なライブラリを入れずに使えて、後述するJSON変換よりも多くのデータ型(Date型やMap, Setなど)に対応しています。

const original = {
  id: 1,
  details: { color: "red", size: "L" },
  created: new Date()
};

// structuredCloneでディープコピー
const deepCopy = structuredClone(original);

// コピー先を変更
deepCopy.details.color = "green";

console.log("original color:", original.details.color);
console.log("copy color:", deepCopy.details.color);

実行結果

original color: red
copy color: green

非常に便利な structuredClone ですが、万能ではありません。

以下のデータはコピーできず、エラーが発生するか意図しない結果になります。

  • 関数(Function):エラーが発生します。
  • DOM要素:エラーが発生します。
  • Symbol:無視されます。
  • プロトタイプチェーン:インスタンスのクラス情報は失われ、プレーンなオブジェクトになります。

JSON.parse(JSON.stringify()) を使う(従来の方法)

古くから使われている「JSON文字列に一度変換してから、オブジェクトに戻す」というテクニックです。
手軽ですが、以下のような制約があります。

  • 関数undefinedSymbol は消滅する。
  • Dateオブジェクトは文字列になる。
  • NaNInfinitynull になる。
  • 循環参照しているオブジェクトを渡すとエラーになる。
const original = {
  id: 2,
  details: { area: "Tokyo" }
};

// JSON変換を経由してディープコピー
const deepCopy = JSON.parse(JSON.stringify(original));

deepCopy.details.area = "Osaka";

console.log(original.details.area); // Tokyo (変更されていない)

JSON.stringify でオブジェクトを文字列化することで参照を断ち切り、JSON.parse で再度オブジェクト化しています。

純粋なJSONデータ(数値、文字列、配列、オブジェクトのみ)であれば問題なく動作します。

パフォーマンスについては、扱うデータの種類やサイズによって structuredClone より速い場合も遅い場合もあるため、状況に応じた判断が必要です。

ライブラリ(Lodash)を使う

大規模な開発で、すでにユーティリティライブラリの Lodash を導入している場合は、cloneDeep メソッドを使うのも一般的です。

// Lodashが必要(バンドルサイズを考慮して個別インポート推奨)
import cloneDeep from 'lodash/cloneDeep';

const deepCopy = cloneDeep(original);

structuredClone が使えない古い環境(レガシーブラウザなど)をサポートする必要がある場合には、こうしたライブラリが頼りになります。

配列のコピーについて

配列(Array)もJavaScriptではオブジェクトの一種であるため、ここまでの話はすべて配列にも当てはまります。

配列のスプレッド構文とディープコピー

配列をコピーする場合も、スプレッド構文 [...] が便利です。
もちろんこれもシャローコピーとなります。

const numbers = [1, 2, 3];
const numbersCopy = [...numbers]; // 配列のシャローコピー

const users = [{ name: "A" }, { name: "B" }];
// 配列の中にオブジェクトがある場合は注意
const usersCopy = [...users]; 
// usersCopy[0].name を変えると users[0].name も変わる(参照の共有)

// 完全なコピーが必要なら structuredClone
const usersDeepCopy = structuredClone(users);

[...numbers] のように配列リテラルの中でスプレッド構文を使うことで、要素を展開して新しい配列を作成できます。

しかし、配列の要素がオブジェクトである場合(users の例)、その中身は参照が共有されたままです。

この場合も structuredClone を使うことで、配列内のオブジェクトごと完全に複製できます。

JavaScriptのスキルを活かして年収を上げる方法

以上、JavaScriptでオブジェクトをコピーする方法について解説してきました。

なお、JavaScriptのスキルがある場合には、「副業で稼いで年収を上げる」といったことが可能です。

JavaScriptのスキルを持つ人は多いものの、その分案件数も多く、副業エージェントやフリーランスエージェントを利用することで予想外の高単価案件が見つかることもあります。

エージェントサービスは、登録も利用もすべて完全無料なので、どんな求人や案件があるのか気になる方は、気軽に利用してみるとよいでしょう。