●シャローコピーとは?
JavaScriptを使っていると、オブジェクトや配列をコピーする場面によく遭遇します。
そんなとき、みなさんはどのようにコピーを行っているでしょうか?単純に=演算子を使ってコピーしていませんか?
実は、これはシャローコピー(shallow copy)と呼ばれる方法で、注意が必要なんです。
○シャローコピーの特徴
シャローコピーの特徴は、コピー元とコピー先が同じメモリ上の参照を持つことです。
つまり、コピー元の値を変更すると、コピー先の値も変わってしまうんです。
これは、参照渡し(call by reference)と同じ動作ですね。
○サンプルコード1:単純なシャローコピー
具体的なコードを見てみましょう。
次のようにオブジェクトをコピーするとどうなるでしょうか。
実行結果を見ると、original
のa
プロパティを変更したら、copy
のa
も変わっています。
これがシャローコピーの特徴です。
○シャローコピーと参照渡しの関係
シャローコピーは、参照渡しと密接な関係があります。
参照渡しは、変数に値そのものを格納するのではなく、値が格納されているメモリ上のアドレスを格納する方式です。
シャローコピーで生成されたコピーは、コピー元と同じメモリアドレスを参照しているため、どちらかを変更すると両方に影響するのです。
こんな感じで、シャローコピーは一見便利そうですが、思わぬ副作用を引き起こすことがあります。
ただ、シャローコピーを適切に使いこなせば、パフォーマンスの向上やコードの簡潔さにつながります。
次は、シャローコピーの具体的な実装方法を見ていきましょう。
●シャローコピーの実例集
さて、シャローコピーの基本的な動作がわかったところで、実際のコードを見ていきましょう。
JavaScriptには、シャローコピーを実現するためのいくつかの方法があります。
ここでは、よく使われるspread構文やObject.assign()、配列のコピー方法について説明します。
○サンプルコード2:spread構文でのシャローコピー
ES2015で導入されたspread構文(…) は、オブジェクトや配列のシャローコピーを簡単に実現できます。
次のようなコードを見てみましょう。
実行結果を見ると、spread構文でコピーしたcopy
は、original
を変更しても影響を受けていません。
ただし、これはあくまでもシャローコピーなので、ネストされたオブジェクトまではコピーされないことに注意が必要です。
○サンプルコード3:Object.assign()でのシャローコピー
Object.assign()メソッドを使っても、オブジェクトのシャローコピーができます。
使い方は次のとおりです。
Object.assign()の第一引数に空のオブジェクト{}を指定し、第二引数にコピー元のオブジェクトを渡します。
これにより、新しいオブジェクトにプロパティがコピーされます。
spread構文と同様に、ネストされたオブジェクトはシャローコピーされることを忘れずに。
○サンプルコード4:配列のシャローコピー
配列をシャローコピーする方法はいくつかありますが、もっとも簡単なのはスライス()メソッドを使う方法です。
slice()メソッドは、もとの配列と同じ要素を持つ新しい配列を返します。
引数を指定しない場合、配列全体がコピーされます。もちろん、spread構文を使っても同じ結果が得られます。
○サンプルコード5:ネストされたオブジェクトのシャローコピー
ここまでの例では、シャローコピーでも問題なく動作していました。
しかし、オブジェクトの中にオブジェクトがネストしている場合はどうでしょうか。
実行結果を見ると、original
のb
プロパティを変更すると、copy
にも影響していることがわかります。
これは、spread構文がネストされたオブジェクトまではコピーしないためです。
同じことは、Object.assign()でも起こります。
●シャローコピーの注意点
シャローコピーは便利な反面、いくつかの注意点があります。
ここでは、参照先の意図しない変更、パフォーマンスの問題、TypeScriptでの型安全性について説明します。
シャローコピーを適切に使いこなすために、これらの注意点を理解しておくことが重要ですね。
○参照先の意図しない変更
シャローコピーでは、コピー元とコピー先が同じ参照を持つため、一方を変更するともう一方にも影響します。
この特性を理解していないと、意図しないバグを引き起こす可能性があります。
例えば、オブジェクトのプロパティを変更するつもりが、そのプロパティが別のオブジェクトを参照していた場合、参照先のオブジェクトまで変更してしまうかもしれません。
こうした問題を避けるためには、シャローコピーとディープコピーの違いを正しく理解し、状況に応じて適切な方法を選ぶ必要があります。
○パフォーマンスの問題
シャローコピーは、参照のコピーだけで済むため、一般的にディープコピーよりも高速です。
しかし、大きなオブジェクトや配列を何度もシャローコピーすると、メモリ使用量が増大し、パフォーマンスが低下する可能性があります。
特に、不必要なシャローコピーを多用すると、メモリリークやガベージコレクションの負荷増大を招くこともあります。
シャローコピーを使う際は、必要最小限にとどめ、コードの効率性にも気を配りましょう。
○TypeScriptでの型安全性
TypeScriptを使っている場合、シャローコピーには型安全性の問題が付きまといます。
スプレッド構文やObject.assign()は、コピー元とコピー先の型が一致していることを保証しません。
例えば、次のようなコードがあるとします。
このコードでは、copy
のage
プロパティに文字列を代入しようとしているため、TypeScriptがエラーを報告します。
こうしたミスを防ぐには、型アサーションを使うか、ジェネリクスを活用してコピーの型を明示的に指定する必要があります。
●よくあるエラーと対処法
シャローコピーを使っていると、予期せぬ動作に悩まされることがあります。
特に、ネストされたオブジェクトや配列を扱う場合、シャローコピーの特性を理解していないと、バグの温床になりかねません。
ここでは、よくあるエラーの例を示し、それを解決するための方法を探ってみましょう。
○サンプルコード6:シャローコピーによる予期せぬ動作
次のようなコードを考えてみます。
このコードでは、original
オブジェクトをcopy
にシャローコピーした後、copy.prop2.nested
の値を変更しています。
しかし、実行結果を見ると、original.prop2.nested
の値も変わっていることがわかります。
これは、prop2
がネストされたオブジェクトで、シャローコピーでは参照がコピーされるだけだからです。
こうした問題を避けるには、ディープコピーを使う必要があります。
ディープコピーでは、ネストされたオブジェクトや配列も再帰的にコピーされるため、コピー元とコピー先が完全に独立します。
○サンプルコード7:ディープコピーを使った解決策
ディープコピーを実現する方法はいくつかありますが、ここではJSONを使った簡単な方法を紹介します。
このコードでは、JSON.stringify()
でoriginal
オブジェクトをJSON文字列に変換し、JSON.parse()
でその文字列を再度オブジェクトに戻しています。
この過程で、ネストされたオブジェクトも新しいオブジェクトとして再作成されるため、ディープコピーが実現されます。
実行結果を見ると、original.prop2.nested
の値は変わらず、copy.prop2.nested
だけが変更されていることがわかります。
これで、シャローコピーによる予期せぬ動作を回避できました。
ただし、JSONを使ったディープコピーには制限があります。例えば、関数やundefinedの値はコピーできません。
より信頼性の高いディープコピーが必要な場合は、サードパーティのライブラリを使うか、再帰的なコピー関数を自作する必要があります。
●シャローコピーの応用例
シャローコピーの特性を理解したところで、実際の開発でどのように活用できるか見ていきましょう。
ここでは、Reactのステート管理を例に、シャローコピーの応用例を紹介します。
Reactは、コンポーネントの状態が変更されると、自動的に再レンダリングを行います。
この際、ステートを不用意に変更すると、パフォーマンスの低下や予期せぬ動作につながります。
シャローコピーを使ったステート管理は、こうした問題を回避するための有効な手段の1つです。
○サンプルコード8:Reactでのステート管理
次のようなReactコンポーネントを考えてみます。
このコンポーネントでは、useState
フックを使ってステートを管理しています。
handleClick
関数内で、setState
を呼び出してステートを更新しています。
ここで注目すべきは、setState
の引数の部分です。
まず、...state
でステートをシャローコピーしています。
これにより、prop1
などのトップレベルのプロパティは、元のステートと同じ参照を保ちます。
次に、prop2
プロパティについては、...state.prop2
でネストされたオブジェクトをシャローコピーし、nested
プロパティだけを変更しています。
こうすることで、prop1
は変更されず、prop2.nested
だけが更新されます。
Reactは、ステートの変更を検知して、必要な部分だけを再レンダリングします。
もし、setState({ prop2: { nested: 'newNestedValue' } })
のように、prop2
を丸ごと新しいオブジェクトで上書きしてしまうと、prop1
も失われてしまいます。
まとめ
さて、JavaScriptのシャローコピーについて、その特徴から実例、注意点、応用例まで幅広く見てきました。
シャローコピーは、オブジェクトや配列を扱う上で欠かせない概念ですが、その動作を正しく理解していないと、思わぬバグに悩まされることがあります。
ただ、JavaScriptのシャローコピーをしっかりと理解することで、より効率的で信頼性の高いコードを書くことができます。
ぜひ今回学んだ知識を活かして、日々の開発に役立ててください。