読み込み中...

JavaScript開発者が知っておくべきシャローコピーの実例8選

JavaScriptのシャローコピーを理解するための実例 JS
この記事は約12分で読めます。

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

本記事のサンプルコードを活用して機能追加、目的を達成できるように作ってありますので、是非ご活用ください。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を満たす現役のプログラマチームによって監修されています。

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

●シャローコピーとは?

JavaScriptを使っていると、オブジェクトや配列をコピーする場面によく遭遇します。

そんなとき、みなさんはどのようにコピーを行っているでしょうか?単純に=演算子を使ってコピーしていませんか?

実は、これはシャローコピー(shallow copy)と呼ばれる方法で、注意が必要なんです。

○シャローコピーの特徴

シャローコピーの特徴は、コピー元とコピー先が同じメモリ上の参照を持つことです。

つまり、コピー元の値を変更すると、コピー先の値も変わってしまうんです。

これは、参照渡し(call by reference)と同じ動作ですね。

○サンプルコード1:単純なシャローコピー

具体的なコードを見てみましょう。

次のようにオブジェクトをコピーするとどうなるでしょうか。

const original = { a: 1, b: 2 };
const copy = original;

console.log(copy); // { a: 1, b: 2 }

original.a = 3;
console.log(copy); // { a: 3, b: 2 }

実行結果を見ると、originalaプロパティを変更したら、copyaも変わっています。

これがシャローコピーの特徴です。

○シャローコピーと参照渡しの関係

シャローコピーは、参照渡しと密接な関係があります。

参照渡しは、変数に値そのものを格納するのではなく、値が格納されているメモリ上のアドレスを格納する方式です。

シャローコピーで生成されたコピーは、コピー元と同じメモリアドレスを参照しているため、どちらかを変更すると両方に影響するのです。

こんな感じで、シャローコピーは一見便利そうですが、思わぬ副作用を引き起こすことがあります。

ただ、シャローコピーを適切に使いこなせば、パフォーマンスの向上やコードの簡潔さにつながります。

次は、シャローコピーの具体的な実装方法を見ていきましょう。

●シャローコピーの実例集

さて、シャローコピーの基本的な動作がわかったところで、実際のコードを見ていきましょう。

JavaScriptには、シャローコピーを実現するためのいくつかの方法があります。

ここでは、よく使われるspread構文やObject.assign()、配列のコピー方法について説明します。

○サンプルコード2:spread構文でのシャローコピー

ES2015で導入されたspread構文(…) は、オブジェクトや配列のシャローコピーを簡単に実現できます。

次のようなコードを見てみましょう。

const original = { a: 1, b: 2 };
const copy = { ...original };

console.log(copy); // { a: 1, b: 2 }

original.a = 3;
console.log(copy); // { a: 1, b: 2 }

実行結果を見ると、spread構文でコピーしたcopyは、originalを変更しても影響を受けていません。

ただし、これはあくまでもシャローコピーなので、ネストされたオブジェクトまではコピーされないことに注意が必要です。

○サンプルコード3:Object.assign()でのシャローコピー

Object.assign()メソッドを使っても、オブジェクトのシャローコピーができます。

使い方は次のとおりです。

const original = { a: 1, b: 2 };
const copy = Object.assign({}, original);

console.log(copy); // { a: 1, b: 2 }

original.a = 3;
console.log(copy); // { a: 1, b: 2 }

Object.assign()の第一引数に空のオブジェクト{}を指定し、第二引数にコピー元のオブジェクトを渡します。

これにより、新しいオブジェクトにプロパティがコピーされます。

spread構文と同様に、ネストされたオブジェクトはシャローコピーされることを忘れずに。

○サンプルコード4:配列のシャローコピー

配列をシャローコピーする方法はいくつかありますが、もっとも簡単なのはスライス()メソッドを使う方法です。

const original = [1, 2, 3];
const copy = original.slice();

console.log(copy); // [1, 2, 3]

original[0] = 0;
console.log(copy); // [1, 2, 3]

slice()メソッドは、もとの配列と同じ要素を持つ新しい配列を返します。

引数を指定しない場合、配列全体がコピーされます。もちろん、spread構文を使っても同じ結果が得られます。

○サンプルコード5:ネストされたオブジェクトのシャローコピー

ここまでの例では、シャローコピーでも問題なく動作していました。

しかし、オブジェクトの中にオブジェクトがネストしている場合はどうでしょうか。

const original = { 
  a: 1,
  b: { c: 2 }
};
const copy = { ...original };

console.log(copy); // { a: 1, b: { c: 2 } }

original.b.c = 3;
console.log(copy); // { a: 1, b: { c: 3 } }

実行結果を見ると、originalbプロパティを変更すると、copyにも影響していることがわかります。

これは、spread構文がネストされたオブジェクトまではコピーしないためです。

同じことは、Object.assign()でも起こります。

●シャローコピーの注意点

シャローコピーは便利な反面、いくつかの注意点があります。

ここでは、参照先の意図しない変更、パフォーマンスの問題、TypeScriptでの型安全性について説明します。

シャローコピーを適切に使いこなすために、これらの注意点を理解しておくことが重要ですね。

○参照先の意図しない変更

シャローコピーでは、コピー元とコピー先が同じ参照を持つため、一方を変更するともう一方にも影響します。

この特性を理解していないと、意図しないバグを引き起こす可能性があります。

例えば、オブジェクトのプロパティを変更するつもりが、そのプロパティが別のオブジェクトを参照していた場合、参照先のオブジェクトまで変更してしまうかもしれません。

こうした問題を避けるためには、シャローコピーとディープコピーの違いを正しく理解し、状況に応じて適切な方法を選ぶ必要があります。

○パフォーマンスの問題

シャローコピーは、参照のコピーだけで済むため、一般的にディープコピーよりも高速です。

しかし、大きなオブジェクトや配列を何度もシャローコピーすると、メモリ使用量が増大し、パフォーマンスが低下する可能性があります。

特に、不必要なシャローコピーを多用すると、メモリリークやガベージコレクションの負荷増大を招くこともあります。

シャローコピーを使う際は、必要最小限にとどめ、コードの効率性にも気を配りましょう。

○TypeScriptでの型安全性

TypeScriptを使っている場合、シャローコピーには型安全性の問題が付きまといます。

スプレッド構文やObject.assign()は、コピー元とコピー先の型が一致していることを保証しません。

例えば、次のようなコードがあるとします。

interface Person {
  name: string;
  age: number;
}

const original: Person = { name: "Alice", age: 30 };
const copy = { ...original, age: "30" }; // エラー: ageはnumber型であるべき

このコードでは、copyageプロパティに文字列を代入しようとしているため、TypeScriptがエラーを報告します。

こうしたミスを防ぐには、型アサーションを使うか、ジェネリクスを活用してコピーの型を明示的に指定する必要があります。

●よくあるエラーと対処法

シャローコピーを使っていると、予期せぬ動作に悩まされることがあります。

特に、ネストされたオブジェクトや配列を扱う場合、シャローコピーの特性を理解していないと、バグの温床になりかねません。

ここでは、よくあるエラーの例を示し、それを解決するための方法を探ってみましょう。

○サンプルコード6:シャローコピーによる予期せぬ動作

次のようなコードを考えてみます。

const original = {
  prop1: "value1",
  prop2: {
    nested: "nestedValue"
  }
};

const copy = { ...original };

copy.prop2.nested = "newNestedValue";

console.log(original.prop2.nested); // "newNestedValue"
console.log(copy.prop2.nested); // "newNestedValue"

このコードでは、originalオブジェクトをcopyにシャローコピーした後、copy.prop2.nestedの値を変更しています。

しかし、実行結果を見ると、original.prop2.nestedの値も変わっていることがわかります。

これは、prop2がネストされたオブジェクトで、シャローコピーでは参照がコピーされるだけだからです。

こうした問題を避けるには、ディープコピーを使う必要があります。

ディープコピーでは、ネストされたオブジェクトや配列も再帰的にコピーされるため、コピー元とコピー先が完全に独立します。

○サンプルコード7:ディープコピーを使った解決策

ディープコピーを実現する方法はいくつかありますが、ここではJSONを使った簡単な方法を紹介します。

const original = {
  prop1: "value1",
  prop2: {
    nested: "nestedValue"
  }
};

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

copy.prop2.nested = "newNestedValue";

console.log(original.prop2.nested); // "nestedValue"
console.log(copy.prop2.nested); // "newNestedValue"

このコードでは、JSON.stringify()originalオブジェクトをJSON文字列に変換し、JSON.parse()でその文字列を再度オブジェクトに戻しています。

この過程で、ネストされたオブジェクトも新しいオブジェクトとして再作成されるため、ディープコピーが実現されます。

実行結果を見ると、original.prop2.nestedの値は変わらず、copy.prop2.nestedだけが変更されていることがわかります。

これで、シャローコピーによる予期せぬ動作を回避できました。

ただし、JSONを使ったディープコピーには制限があります。例えば、関数やundefinedの値はコピーできません。

より信頼性の高いディープコピーが必要な場合は、サードパーティのライブラリを使うか、再帰的なコピー関数を自作する必要があります。

●シャローコピーの応用例

シャローコピーの特性を理解したところで、実際の開発でどのように活用できるか見ていきましょう。

ここでは、Reactのステート管理を例に、シャローコピーの応用例を紹介します。

Reactは、コンポーネントの状態が変更されると、自動的に再レンダリングを行います。

この際、ステートを不用意に変更すると、パフォーマンスの低下や予期せぬ動作につながります。

シャローコピーを使ったステート管理は、こうした問題を回避するための有効な手段の1つです。

○サンプルコード8:Reactでのステート管理

次のようなReactコンポーネントを考えてみます。

import React, { useState } from 'react';

const MyComponent = () => {
  const [state, setState] = useState({
    prop1: 'value1',
    prop2: {
      nested: 'nestedValue'
    }
  });

  const handleClick = () => {
    setState({
      ...state,
      prop2: {
        ...state.prop2,
        nested: 'newNestedValue'
      }
    });
  };

  return (
    <div>
      <p>{state.prop1}</p>
      <p>{state.prop2.nested}</p>
      <button onClick={handleClick}>Change nested value</button>
    </div>
  );
};

export default MyComponent;

このコンポーネントでは、useStateフックを使ってステートを管理しています。

handleClick関数内で、setStateを呼び出してステートを更新しています。

ここで注目すべきは、setStateの引数の部分です。

まず、...stateでステートをシャローコピーしています。

これにより、prop1などのトップレベルのプロパティは、元のステートと同じ参照を保ちます。

次に、prop2プロパティについては、...state.prop2でネストされたオブジェクトをシャローコピーし、nestedプロパティだけを変更しています。

こうすることで、prop1は変更されず、prop2.nestedだけが更新されます。

Reactは、ステートの変更を検知して、必要な部分だけを再レンダリングします。

もし、setState({ prop2: { nested: 'newNestedValue' } })のように、prop2を丸ごと新しいオブジェクトで上書きしてしまうと、prop1も失われてしまいます。

まとめ

さて、JavaScriptのシャローコピーについて、その特徴から実例、注意点、応用例まで幅広く見てきました。

シャローコピーは、オブジェクトや配列を扱う上で欠かせない概念ですが、その動作を正しく理解していないと、思わぬバグに悩まされることがあります。

ただ、JavaScriptのシャローコピーをしっかりと理解することで、より効率的で信頼性の高いコードを書くことができます。

ぜひ今回学んだ知識を活かして、日々の開発に役立ててください。