【ITニュース解説】Assignment by Value vs Assignment by Reference in JavaScript
2025年09月24日に「Dev.to」が公開したITニュース「Assignment by Value vs Assignment by Reference in JavaScript」について初心者にもわかりやすく解説しています。
ITニュース概要
JavaScriptで変数を代入する際、プリミティブ型は値がコピーされ、元の変数は変更されない。しかしオブジェクト型は参照がコピーされるため、一方を変更するともう一方も変わる。これを防ぐにはスプレッド演算子などで新しいオブジェクトを作成する必要がある。この「値渡し」と「参照渡し」の違いを理解することがバグ防止に繋がる。
ITニュース解説
JavaScriptでプログラミングを行う際、let x = y のようにある変数の値を別の変数に代入する場面は頻繁に登場する。多くの場合、これは y の値が x にコピーされ、x と y がそれぞれ独立した値を持つようになると直感的に理解されるだろう。しかし、時には y を変更すると、なぜか x も同時に変化してしまうという予期せぬ挙動に遭遇することがある。この現象は、JavaScriptにおける「値渡し(Assignment by Value)」と「参照渡し(Assignment by Reference)」という、プログラミングの根幹をなす重要な概念を理解することで明確になる。変数の代入方法は見た目上は同じでも、その舞台裏で何が起きているかは、扱うデータの種類によって大きく異なる。この違いを把握することは、多くの不可解なバグを防ぎ、より堅牢なコードを書くための第一歩となる。
まず、JavaScriptのデータ型は大きく「プリミティブ型」と「オブジェクト型」に分けられる。この二つのデータ型で、変数の代入時の挙動が根本的に異なる。
プリミティブデータ型には、文字列(string)、数値(number)、真偽値(boolean)、null、undefinedなどが含まれる。これらのデータ型は「イミュータブル(immutable)」、つまり一度作成された値は後から変更できないという特性を持つ。プリミティブ型の値をある変数から別の変数へ代入する場合、JavaScriptは元の値の新しい独立したコピーを作成する。
具体的な例で考えてみよう。
let x = 10;
let y = x;
この段階では、x と y はどちらも数値の 10 を保持している。コンピュータのメモリ上では、まず x という変数がメモリのある場所に 10 という値を格納し、その場所を指し示す。次に y = x が実行されると、x が保持する値 10 が完全にコピーされ、メモリの別の場所に 10 として格納され、y はその新しい場所を指し示すようになる。つまり、この時点で x と y はそれぞれメモリ上の異なる場所にある 10 という値を指している。
ここで、y の値を変更してみる。
y = 20;
この操作は、y が現在指しているメモリの場所の値を 20 に変更するのではなく、y が新しい値 20 が格納されたメモリの別の場所を指すようにポインタを移動させることを意味する。なぜなら、プリミティブ型はイミュータブルだからだ。結果として、x は元の 10 を指したままであり、y は新しく 20 を指すようになる。つまり、x の値は 10 のままで、y の値は 20 になる。このように、プリミティブ型においては、一方の変数を変更してももう一方の変数には一切影響が及ばない。
次に、オブジェクト型について見てみよう。JavaScriptにおけるオブジェクト型には、通常のオブジェクトのほか、配列(array)や関数(function)も含まれる。これらのデータ型は「ミュータブル(mutable)」、つまり作成後にその内部のプロパティを変更できるという特性を持つ。オブジェクト型をある変数から別の変数へ代入する場合、JavaScriptはプリミティブ型のようにオブジェクト全体をコピーするのではなく、そのオブジェクトがメモリ上に存在する「場所(メモリアドレス)」への「参照」をコピーする。この結果、代入を行った両方の変数は、メモリ上の「同じオブジェクト」を指し示すことになる。
例を見てみよう。
let user = { name: "John", role: "user" };
let admin = user;
最初の行で、user という変数がメモリ上に { name: "John", role: "user" } というオブジェクトを作成し、user はそのオブジェクトのメモリアドレスを指す。次の行 let admin = user が実行されると、admin 変数に user が指しているメモリアドレスの「参照」がコピーされる。これにより、user と admin の両方が、メモリ上の「同じ一つのオブジェクト」を指す状態になる。
ここで、admin 変数を使ってオブジェクトのプロパティを変更してみる。
admin.role = "admin";
この操作は、admin が指し示しているメモリ上のオブジェクトに直接アクセスし、その role プロパティの値を "admin" に変更することを意味する。そして、user も同じオブジェクトを指しているため、user からオブジェクトを参照しても、role の値が "admin" に変更されていることが確認できる。このように、オブジェクト型では、一方の変数を介してオブジェクトのプロパティを変更すると、他方の変数にもその変更が反映されるという現象が起こる。これは、両者が同じ実体を見ているためである。
では、オブジェクトの内容をコピーし、元のオブジェクトとのリンクを断ち切るにはどうすれば良いだろうか。つまり、変更が互いに影響しないように、メモリ上の異なるアドレスに独立したオブジェクトを作成したい場合だ。
一つの方法は、新しいオブジェクトを明示的に作成し、元のオブジェクトのプロパティを一つずつコピーする方法である。
let user = { name: "John", role: "user" };
let admin = { name: "John", role: "user" };
この場合、admin は user とは完全に独立した新しいオブジェクトをメモリ上の異なるアドレスに作成する。この方法では、admin.role = "admin"; と変更しても user には影響しない。しかし、オブジェクトのプロパティが非常に多い場合、手動で全てのプロパティを記述するのは冗長であり、記述ミスや抜け落ちの原因にもなりかねない。
よりスマートな方法として、ES2015で導入された「スプレッド演算子 (...)」を使用することが一般的だ。
let user = { name: "John", role: "user" };
let admin = { ...user };
この let admin = { ...user }; という記述は、user オブジェクトの全てのプロパティを展開し、それらのプロパティを持つ「新しいオブジェクト」をメモリ上の異なるアドレスに作成することを意味する。これにより、user と admin はそれぞれメモリ上の異なるオブジェクトを指すようになり、独立した存在となる。したがって、admin.role = "admin"; と変更しても user オブジェクトには影響が及ばず、元の状態を保つことができる。
ただし、スプレッド演算子によるコピーは「シャローコピー(浅いコピー)」である点に注意が必要だ。これは、オブジェクトがネスト(入れ子)構造になっている場合、一番外側のレベルのプロパティは値がコピーされるが、ネストされたオブジェクトや配列のプロパティについては、その「参照」がコピーされるに過ぎないということを意味する。つまり、ネストされたオブジェクトの内容を変更すると、元のオブジェクトのネストされた部分にも影響が及んでしまう可能性がある。
最後に、JavaScriptにおける変数の「比較」についても、データ型によって挙動が異なる。
プリミティブ型の場合、比較は「値」に基づいて行われる。例えば、10 === 10 は true となる。
しかし、オブジェクト型の場合、比較は「参照」に基づいて行われる。つまり、たとえ2つのオブジェクトが全く同じプロパティと値を持っていても、それらがメモリ上の「同じアドレス」を指していない限り、JavaScriptはそれらを異なるオブジェクトと判断する。
let user = { name: "John", role: "user" };
let admin = { ...user };
この user と admin は、どちらも全く同じ内容のオブジェクトに見えるが、console.log(user === admin); の結果は false となる。これは、スプレッド演算子によって admin が user とは異なるメモリアドレスに新しいオブジェクトを作成したため、両者が異なる参照を指していることに起因する。
結論として、JavaScriptで堅牢でバグのないコードを書くためには、各データ型がどのように変数に代入され、メモリ上でどのように扱われるかを深く理解することが不可欠である。プリミティブ型では、代入も比較も「値」に基づいて行われ、常に独立したコピーが作成されるため、一方の変数の変更が他方に影響することはない。一方、オブジェクト型では、代入も比較も「参照」に基づいて行われる。これは、複数の変数が同じメモリ上のオブジェクトを指し示すことを意味するため、一方の変数を通じて行われた変更は、同じオブジェクトを指すすべての変数に反映される。この共有された参照を意図的に断ち切りたい場合は、スプレッド演算子 (...) を用いたシャローコピーが有効な手段となる。ただし、ネストされたオブジェクトに対する影響を考慮する必要がある。値のコピーと参照の共有という根本的な違いを把握することは、プログラミングにおける予期せぬ挙動を回避し、コードの意図を明確にする上で非常に重要なスキルとなる。