JavaScriptを使っていると「参照渡しと値渡しの違いは何か」という疑問に直面することが多くあります。特にオブジェクトや配列を扱う際に、関数内での変更が元の変数にも影響するケースと影響しないケースがあり、この境界線が分かりにくいためです。この記事では JavaScript の「参照 渡し」と「値 渡し」の違いに焦点を当て、本質を理解できるよう丁寧に整理します。初心者から中級者まで、実践で混乱しない理解が得られる内容を目指しています。
目次
JavaScript 参照 渡し 値 渡し 違いとは何か
JavaScript において「参照渡し」と「値渡し」がどのような概念であり、どのように動くのかを明らかにすることは、コードのバグを減らし意図しない副作用を防ぐために非常に重要です。ここではまず基本的な定義と、どのデータ型がどちらに該当するかを見て行きます。
値渡し(Pass by Value)の定義
値渡しとは、関数に引数を渡す際にその値のコピーを渡す方式です。つまり、関数側で引数として受け取った変数を変更しても、元の変数には影響しません。JavaScript のプリミティブ型、つまり数値・文字列・真偽値・undefined・null・symbol・bigint はこの値渡しで扱われます。これらは不変(イミュータブル)であるため、コピーが安全に扱えます。
参照渡し(Pass by Reference/参照型)の定義
参照渡しでは、関数に渡されるのが値そのものではなく、その値への「参照(アドレス)」です。JavaScript ではオブジェクト・配列・関数などの複合型データがこれに当たります。関数および関数外で同じ対象を指し示すため、関数内でプロパティを変更すると元のオブジェクトにもその変更が反映されます。ただし、参照そのものを別のオブジェクトに再代入しても呼び出し元の参照は変わりません。
誤解されやすいポイント
JavaScript では「オブジェクトが参照渡し」という表現が一般的ですが、正確には「参照を値として渡している」という方式です。参照渡しの伝統的な意味では、関数内で変数そのものを呼び出し元と共有できる場合を指しますが、JavaScript ではこれができません。この違いを理解することが混乱を避ける鍵です。
プリミティブ型と参照型の挙動比較
Primitives(プリミティブ型)と Objects(参照型)では、同じ操作をした場合でも挙動が大きく異なります。この章では具体的なコード例を通じて、その違いを視覚的・論理的に理解します。
プリミティブ型での値の代入と関数への渡し方
数値や文字列などプリミティブ型の変数を別の変数に代入すると、完全なコピーが作成されます。関数に渡した場合も同様で、関数内で引数を変更しても外側の値は変わりません。これはプリミティブ型が不変であり、値そのものが直接扱われているからです。
参照型での代入と関数経由でのプロパティ変更
オブジェクトや配列を別の変数に代入すると、両方の変数が同じオブジェクトへの参照を持ちます。関数にそのオブジェクトを渡し、プロパティを変更すると、その変更は呼び出し元にも反映されます。これは「参照をコピーして渡す」方式であり、共有されたデータに対する操作が影響を及ぼします。
参照型で再代入したときの振る舞い
関数内で参照型の引数を別のオブジェクトに再代入すると、呼び出し元の参照は変わりません。つまり、引数変数が指し示すものを変えても、外側の変数は依然として元のオブジェクトを指したままです。これは「参照の再代入」が関数スコープ内だけで有効であるためです。
実践で混乱しやすい例とその回避方法
実際にコードを書くとき、参照渡し・値渡しに関する誤解がよく生じます。それらの例を見て、どうすれば混乱を避けるかを具体的に考察します。安全で予測可能なプログラミングのための工夫も紹介します。
副作用の問題:意図せぬオブジェクトの変更
配列やオブジェクトを関数に渡し、その中身を操作することは強力ですが、それによって他のコードで同じオブジェクトを使っていた場合には意図しない影響が及ぶことがあります。このような副作用を避けるためには、コピーを作って操作を行う方法が有効です。
浅いコピーと深いコピーの使い分け
参照型をコピーする場合、浅いコピー(shallow copy)は最上層のプロパティだけをコピーし、ネストされたオブジェクトは元と共有します。一方、深いコピー(deep copy)は全階層を複製し、完全に独立したデータにします。最新の JavaScript では構造化 clone 関数などを使って深いコピーが可能です。
const・let・再代入と参照型の関係
const を使ってオブジェクトを宣言しても、そのオブジェクトのプロパティは変更可能です。const は変数名への再代入(ラベルが別のオブジェクトを指すようにすること)を禁止するだけであり、参照先の内容変更を禁止するわけではありません。これによって「const だから安全」という誤解が生まれることがあります。
仕様から見た JavaScript の引数渡しの本質
JavaScript が ECMAScript 規格でどのように引数を扱うかについて見て行きます。特にプリミティブ型・参照型がメモリ上でどのように配置され、具体的に何がコピーされるかを理解することで、「参照渡し vs 値渡し」の真の意味が分かります。
ECMAScript 規格におけるプリミティブ型の取り扱い
プリミティブ型は不変であり、値そのものがスタックまたはリテラルとして保存されます。関数の呼び出し時にはその値のコピーが渡され、関数内での変更は呼び出し元の変数には影響しません。これは仕様上明確に定義された挙動です。
参照値としてのオブジェクト・配列の扱われ方
オブジェクト型や配列は参照値として扱われます。変数は実データの場所を指す参照(アドレス)を「値として」持っており、これをコピーして引数として渡します。そのため関数内でプロパティのミューテーションが可能になりますが、参照そのものを別のものに再割当てしても外側には影響しません。
call-by-sharing と混同されやすい概念
JavaScript のオブジェクト渡しは、評価戦略として「call-by-sharing(共有参照による値渡し)」とされることがあります。これは、参照そのもののコピーを渡す方式であり、高級言語で一般的な参照渡しとは異なります。混同すると、関数での再代入とプロパティ変更との差異を見落としてしまいます。
参照渡しと値渡しの誤用を防ぐためのベストプラクティス
参照渡しと値渡しを正しく使い分け、意図しない副作用を避けるための具体的なテクニックと習慣を紹介します。生産性とコードの品質を両立させるための手法です。
イミュータビリティを意識する設計
オブジェクトを不変に扱うことで予期せぬ変更を防ぐことができます。例えばオブジェクトを変更する代わりに、新しいオブジェクトを返す関数を使うようにすると、参照型の副作用を排除できます。Immutable パターンや純関数を意識することが有効です。
浅いコピー・深いコピーの具体的な実装手段
浅いコピーにはスプレッド構文、Object.assign、配列の slice メソッドなどがあります。深いコピーには構造化 clone や JSON を経由する方法などがあります。ただし JSON を使った深いコピーには関数や undefined が失われるなどの制約があるため、状況に応じて選択することが大切です。
API 設計・関数設計における引数の扱い方を明示する
関数の引数が「mutable なオブジェクト」である場合、それが変更されるかどうかをドキュメントに明記したり、変更されるならコピーを受け取る設計にすることで安全性が上がります。またライブラリ設計では、呼び出し元に副作用を与えないことが望まれることが多いため、参照を直接触る場合は注意深く設計するべきです。
表で比較する参照渡しと値渡しの違い
ここまでの理解を補強するため、参照渡しと値渡しの主な性質を表形式で整理します。どの特徴がどちらに対応するか一目で分かる一覧表です。
| 特徴 | 値渡し(プリミティブ型) | 参照渡し的振る舞い(参照型) |
|---|---|---|
| 対象 | 数値・文字列・真偽値・undefined・null・symbol・bigint | オブジェクト・配列・関数などの複合型 |
| 関数内変更が外側に影響するか | いいえ | プロパティ変更は影響あり、再代入は影響なし |
| コピー作成の方法 | 直接コピー値を保持 | 参照をコピー、データそのものは共有 |
| 再代入で元の変数が変わるか | 関係なし(プリミティブなので元の変数に直接作用しない) | 再代入は関数スコープ内だけ影響、呼び出し元には影響しない |
| コピーが浅いか深いか | 該当なし | 浅いコピーではネスト共有、深いコピーでは完全複製 |
実用的なコード例と理解のためのケーススタディ
具体的なコード例を通じて、参照渡しと値渡しの違いを操作で確かめます。関数定義・実際の呼び出し・結果の違いを見て、自分のコードでどちらの挙動を期待すべきかを判断できるようにします。
プリミティブ型の例:値を変更してもオリジナルは変わらない
例えば、次のようなコードがあったとします。数値を引数に取り、それを変える関数です。関数内部では引数を別の数値に変更しますが、呼び出し元の変数は影響を受けません。これは値渡しの典型例であり、プリミティブ型の特性が働いています。
参照型の例:プロパティの変更が呼び出し元に影響する
オブジェクトを引数に取り、そのプロパティを変更する関数を考えます。関数内部で置き換えをせずプロパティを直接操作すると、呼び出し元のオブジェクトにもその変更が反映されます。これは参照値の共有による振る舞いです。
再代入の例:参照型でも参照そのものを変えるとどうなるか
参照型引数を関数内で新しいオブジェクトに再代入しても、その変更は呼び出し元には影響しません。引数変数の参照を切り替える操作はスコープ内だけで有効であり、呼び出し元の参照は保持されます。この差異を明確に理解することが大切です。
まとめ
JavaScript における「参照渡し」と「値渡し」の違いを整理すると、「プリミティブ型は値渡し」という点と、「参照型は参照を値として渡すため、プロパティ変更が呼び出し元に影響するが再代入は影響しない」という点が核心です。多くの混乱はこの再代入とプロパティ変更の違いを見落とすことから生じます。
コードを書く際は、引数がプリミティブか参照型かを意識し、変更が呼び出し元に及ぶ可能性を考えて設計することが重要です。必要なら浅いコピーや深いコピーを明示的に使い、副作用を制御する設計を心掛けましょう。
これらの理解を通じて、JavaScript での値のやり取りに自信を持てるようになり、より堅牢で予測可能なコードを書くことができるようになります。
コメント