JavaScriptを学び始めたばかりの頃、変数に代入した値が予想外の挙動を示して驚いた経験はないでしょうか。
数値や文字列を扱っているときは直感通りに動くのに、配列やオブジェクトを扱い始めると、一つの変数を変更したはずが別の変数まで書き換わってしまうことがあります。
この現象の背後には、JavaScriptにおける「プリミティブ型」と「参照型」という2つのデータ型の本質的な違いが隠れています。
本記事では、メモリ管理の仕組みからコードの書き方における注意点まで、プログラミングの現場で必須となる知識を詳しく紐解いていきます。
JavaScriptにおけるデータ型の分類
JavaScriptのデータ型は、大きく分けて「プリミティブ型」と「参照型」の2つのグループに分類されます。
プログラムがデータをどのように保持し、メモリ上でどのように扱うかを理解するためには、まずそれぞれのグループに属する型を把握することが不可欠です。
プリミティブ型とは
プリミティブ型は、「値そのもの」を直接保持する単純なデータ型のことです。
一度作成されると、その値自体を変更することはできない「不変(イミュータブル)」という性質を持っています。
JavaScriptには、以下の7つのプリミティブ型が存在します。
- 数値型 (Number):整数や浮動小数点数
- 文字列型 (String):テキストデータ
- 論理型 (Boolean):true または false
- undefined:値が未定義であることを示す
- null:値が空であることを明示的に示す
- シンボル型 (Symbol):一意で不変な識別子
- BigInt型:非常に大きな整数
参照型(オブジェクト型)とは
参照型は、「データの格納場所(メモリ上のアドレス)」を保持する複雑なデータ型です。
プリミティブ型とは対照的に、値の内部を自由に書き換えることができる「可変(ミュータブル)」な性質を持っています。
具体的には、以下のようなものが参照型に該当します。
- オブジェクト (Object)
- 配列 (Array)
- 関数 (Function)
- 日付 (Date) や正規表現 (RegExp) など
メモリの仕組み:スタック領域とヒープ領域
なぜプリミティブ型と参照型で挙動が変わるのか、その理由はコンピュータの「メモリ管理」の仕組みにあります。
JavaScriptエンジンは、データを保存するために「スタック(Stack)」と「ヒープ(Heap)」という2つの領域を使い分けています。
スタック領域での管理
スタック領域は、サイズが固定されたデータを高速に処理するためのメモリ空間です。
プリミティブ型の変数は、このスタック領域に「値そのもの」が直接保存されます。
そのため、変数を別の変数に代入する際は、値がそのままコピーされることになります。
ヒープ領域での管理
ヒープ領域は、データのサイズが動的に変化する可能性がある、より複雑なデータを保存するための広い空間です。
参照型のデータ本体は、このヒープ領域に保存されます。
スタック領域にある変数には、ヒープ領域にあるデータ本体がどこにあるのかを示す「住所(メモリ参照アドレス)」だけが記録されます。
| 特徴 | プリミティブ型 | 参照型 |
|---|---|---|
| 保存場所 | スタック領域(値そのもの) | ヒープ領域(本体)とスタック(参照先) |
| 代入の挙動 | 値のコピー | 参照(アドレス)のコピー |
| 変更の可否 | 不変(イミュータブル) | 可変(ミュータブル) |
| 比較方法 | 値の内容で比較 | 参照先のアドレスで比較 |
「値のコピー」と「参照のコピー」の違い
変数から変数へデータを代入するときの挙動を、コードを通して具体的に確認してみましょう。
この違いを正しく理解していないと、大規模な開発においてデバッグの難しいバグを生む原因となります。
プリミティブ型の場合:値のコピー
プリミティブ型では、代入によって「値の複製」が行われます。
let a = 10;
let b = a; // 値「10」がコピーされる
b = 20; // bを書き換えてもaには影響しない
console.log(a);
console.log(b);
10
20
変数 a の値を変数 b に代入した時点で、b には独立した新しい「10」という数値が格納されます。
したがって、後から b の値を変更しても a の値が連動して変わることはありません。
参照型の場合:参照のコピー
参照型では、代入によって「メモリ上のアドレス(参照先)」がコピーされます。
let user1 = { name: "Tanaka" };
let user2 = user1; // アドレス(参照先)がコピーされる
user2.name = "Sato"; // user2を通じて中身を書き換える
console.log(user1.name);
console.log(user2.name);
Sato
Sato
変数 user1 に代入されたオブジェクトの「実体」はヒープ領域に一つしかありません。
user2 = user1 としたとき、コピーされたのは実体ではなく、実体へと繋がる「住所」だけです。
「同じ住所」を指している2つの変数があるため、一方を操作するともう一方から見える値も変わってしまうのです。
比較演算子(===)における挙動の差
JavaScriptで厳密等価演算子(===)を使用する際も、型による違いに注意が必要です。
一見同じ内容に見えるデータであっても、型の性質によって結果が異なる場合があります。
プリミティブ型の比較
プリミティブ型の場合、「保持している値の内容」が等しければ true となります。
let str1 = "Hello";
let str2 = "Hello";
console.log(str1 === str2);
true
参照型の比較
参照型の場合、「同じメモリ上のアドレスを参照しているか」が判定基準となります。
そのため、たとえプロパティの内容が全く同じオブジェクトであっても、別々に作成されたものであれば結果は false になります。
let obj1 = { id: 1 };
let obj2 = { id: 1 };
console.log(obj1 === obj2); // 異なるメモリ位置なのでfalse
false
反対に、先ほどの代入の例のように、同じ参照をコピーした変数同士であれば true と判定されます。
let obj3 = obj1;
console.log(obj1 === obj3); // 同じメモリ位置を参照しているのでtrue
true
参照型の注意点:関数の引数
関数に引数を渡す際も、参照型の性質を忘れてはいけません。
関数内で受け取ったオブジェクトを変更すると、関数を呼び出した側の元のデータまで書き換わってしまいます。
function updateAge(person) {
person.age = 25; // 引数として渡されたオブジェクトのプロパティを変更
}
const myProfile = { name: "Suzuki", age: 20 };
updateAge(myProfile);
console.log(myProfile.age);
25
これは「副作用」と呼ばれ、意図しないデータの不整合を引き起こす大きな要因となります。
安全なコードを書くためには、元のオブジェクトを直接操作せず、コピーを作成してから操作することが推奨されます。
オブジェクトの安全なコピー方法(浅いコピーと深いコピー)
参照型のデータを他の変数に安全に渡したい場合は、参照のコピーではなく「データの複製(クローン)」を行う必要があります。
これには「浅いコピー(Shallow Copy)」と「深いコピー(Deep Copy)」の2つのアプローチがあります。
スプレッド構文による浅いコピー
モダンなJavaScript(ES6以降)では、スプレッド構文(...)を使うことで簡単に新しいオブジェクトを作成できます。
const original = { a: 1, b: 2 };
const copy = { ...original };
copy.a = 99;
console.log(original.a); // 1 (元のオブジェクトは影響を受けない)
1
ただし、浅いコピーには「ネストされた(階層構造を持つ)オブジェクトまでは独立させられない」という制約があります。
オブジェクトの中にあるオブジェクトは、依然として同じ参照を指したままになるため注意が必要です。
structuredClone による深いコピー
多階層のオブジェクトを完全に独立した状態でコピーしたい場合は、モダンなブラウザやNode.jsで標準搭載されている structuredClone 関数を利用するのが最も確実です。
const complexObj = {
id: 1,
info: { name: "Alice" }
};
const deepCopy = structuredClone(complexObj);
deepCopy.info.name = "Bob";
console.log(complexObj.info.name); // "Alice" のまま保持される
Alice
これにより、内部のオブジェクトを含めてすべて新しいメモリ領域に複製されるため、副作用の心配がない安全なプログラミングが可能になります。
まとめ
JavaScriptにおけるプリミティブ型と参照型の違いを理解することは、エンジニアとしての基礎を固める上で非常に重要です。
プリミティブ型はスタック領域で値を直接扱い、不変であるため、直感的な操作が可能です。
一方で、参照型はヒープ領域にあるデータの「住所」を介して操作されるため、代入や比較において特有のルールが存在します。
「なぜこの変数の値が変わってしまったのか?」と悩んだときは、そのデータがどちらの型に属しているかをまず確認してみてください。
メモリの仕組みを意識し、適切にオブジェクトのコピーを行う習慣をつけることで、バグの少ない堅牢なアプリケーションを構築できるようになるでしょう。
日々のコーディングにおいて、今回学んだ「参照」の概念を常に意識し、意図しないデータの書き換えを防ぐ工夫を取り入れていきましょう。
