こんにちは!フロントエンドからNode.jsの内部挙動まで、日々JavaScriptと向き合っていると、「変数って、一体どこにどうやって保存されているんだろう?」という疑問に行き着く瞬間がやってきますよね。
他のプログラミング言語、例えばC++やJavaを触ったことがある方なら、「メモリ管理」や「スタックとヒープ」という言葉を聞いてドキッとしたかもしれません。「JavaScriptはガベージコレクタ(GC)が勝手にやってくれるから意識しなくていいんでしょ?」と思われがちですが……実は、本番環境でパフォーマンスを極限まで引き出すためには、このメモリの裏側を知っているかどうかが決定的な分かれ道になります。
ここをクリアすれば、JavaScriptのデータ構造の振る舞いが手に取るように分かるようになり、ワンランク上のエンジニアへとステップアップできますよ。それでは、メモリの最深部を覗く旅に出発しましょう!
—
1. JavaScriptのメモリ空間:スタック(Stack)とヒープ(Heap)の世界
JavaScriptのコードが実行されるとき、V8などのJavaScriptエンジンは、主に2つのメモリ領域を使ってデータを管理します。それが「コールスタック(Stack)」と「ヒープ(Heap)」です。
イメージとしてはこうです:
- スタック領域:整理整頓されたロッカーのようなもの。上から順に綺麗にデータが積まれ、取り出しも超高速。ただし、入るデータの「サイズ(大きさ)」が決まっている必要があります。
- ヒープ領域:巨大な倉庫のようなもの。どこに何を置いても自由ですが、荷物の場所(番地)をメモしておかないと行方不明になりやすい場所です。こちらはサイズが可変の複雑なデータを扱います。
この2つの領域に、JavaScriptのデータ型がどのように振り分けられるのかを見ていきましょう。
—
2. プリミティブ型は「スタック」に住み、オブジェクト型は「ヒープ」に住む
JavaScriptのデータ型は、大きく「プリミティブ型(基本型)」と「オブジェクト型(参照型)」に分かれます。メモリの観点から見ると、この2者は住んでいる場所がまったく異なります。
プリミティブ型(数値、文字列、真偽値、`null`, `undefined`, Symbol, BigInt)
これらはサイズが固定されており、非常に軽量です。そのため、原則としてスタック領域にその「値そのもの」が直接保存されます。
// プリミティブ型の宣言
let score = 100; // 数値型(スタックに配置)
let playerName = “Kei”; // 文字列型(スタックに配置)
let isGameClear = false; // 真偽値型(スタックに配置)
ここで重要なポイントは、プリミティブ型は「値渡し」されるということです。
let originalScore = 50;
let copiedScore = originalScore; // 値のコピーがスタックに新しく作られる
copiedScore = 80;
console.log(originalScore); // 50 (影響を受けない!)
console.log(copiedScore); // 80
`copiedScore` を書き換えても、`originalScore` は微動だにしません。それぞれがスタック上で完全に独立した部屋に住んでいるからです。
オブジェクト型(オブジェクト、配列、関数など)
一方で、配列やオブジェクト、関数といったデータは、中身が増減したり構造が複雑だったりするため、サイズをあらかじめスタックに固定できません。
そのため、実体(データ本体)はヒープ領域という巨大な倉庫にドンと置かれます。そして、スタック側には「ヒープのどこにそのデータがあるか」を示す住所(参照アドレス)だけが保存されます。だからこれらは「参照型」と呼ばれるわけですね。
// オブジェクト型の宣言
let playerA = {
name: “Kei”,
score: 100
};
// 実体は「ヒープ領域」に作られ、
// その「住所(ポインタ)」が「スタック領域」の playerA に保存される
—
3. 陥りやすい罠:参照型のコピーと予期せぬバグ
初心者が最もハマりやすい落とし穴が、この「参照型」のメモリの仕組みに起因するバグです。実際のコードを見てみましょう。
// プレイヤーAのデータを別の変数に代入してみる
let playerA = { name: “Kei”, score: 100 };
let playerB = playerA; // ここで何が起きている?
// プレイヤーBのスコアを変更してみる
playerB.score = 200;
console.log(playerA.score); // 予想:100? 実結果:200!?
console.log(playerB.score); // 200
「えっ、`playerB` を変えたのに、なんで `playerA` まで変わっちゃったの!?」とパニックになる瞬間ですね。
メモリの視点からこれを脳内トレースしてみましょう。
1. `let playerB = playerA;` と書いたとき、コピーされたのはオブジェクトの中身そのものではなく、「ヒープ上の住所(参照)」です。
2. つまり、スタック上には `playerA` と `playerB` という2つの変数が存在しますが、どちらもヒープ領域にある全く同じオブジェクトの住所を指し示しています。
3. だから `playerB.score = 200;` と操作すると、共通の住所にあるオブジェクト本体が書き換わり、`playerA` から覗き見てもスコアが200に変わっているというわけです。
これが、バグの温床になりやすい「意図しない参照の共有」です。
—
4. ガベージコレクション(GC)の効率を最大化するデータ構造の選び方
さて、ここまでメモリの配置を見てきたあなたなら、JavaScriptの「ガベージコレクション(GC)」がどのように働くかもイメージしやすいはずです。
V8エンジンなどのガベージコレクタは、定期的にメモリを走査し、「どの変数からも参照されなくなった(=スタックから住所が消えた)ヒープ上のデータ」を自動的に掃除(解放)してくれます。
GCに優しいコードを書くための実践知見
1. 不要になった参照は速やかに断ち切る
グローバル変数や長期間生き続ける配列・オブジェクトの中に、用が済んだ大きなオブジェクトを入れっぱなしにすると、GCが「まだ使われているかもしれない」と判断し、メモリを解放できません(これがメモリリークの典型例です)。
使い終わった大きなデータは、`myVariable = null;` のように明示的に参照を断ち切ることで、GCの回収対象にしてあげましょう。
2. 不変性(イミュータビリティ)を意識する
モダンなJavaScript(ReactやVueなどのエコシステム)で、オブジェクトを直接書き換える(ミュータブルな操作)のではなく、スプレッド構文(`…`)などを使って新しいオブジェクトを作り直す(イミュータブルな操作)ことが推奨されるのは、まさにこのメモリ管理と予測可能性を高めるためです。
let playerA = { name: “Kei”, score: 100 };
// ❌ 良くない例:直接書き換え(参照が維持されるため追跡しにくい)
playerA.score = 200;
// ⭕ 良い例:スプレッド構文で新しいオブジェクトをヒープに生成する
let updatedPlayerA = { …playerA, score: 200 };
もちろん、毎回新しいオブジェクトを作るとヒープ領域のメモリ割り当て・解放(GCの頻度)が増えますが、現代のV8エンジンのジェネレーショナルGC(世代別ガベージコレクション)は非常に優秀です。短期生存するオブジェクト(すぐにゴミになるオブジェクト)の回収は高速に行われるため、コードの予測可能性(バグのなさ)を優先する設計がベストプラクティスとなります。
—
まとめ:メモリが見えれば、コードが透けて見える
- プリミティブ型は、サイズが固定されているためスタック領域に住み、値そのものがコピーされる。
- オブジェクト型は、サイズが変動するため実体はヒープ領域に住み、スタックにはその「住所」だけが置かれる。
- 参照のコピーによる意図しないデータ書換に注意し、不要な参照は断ち切ることでGCの効率を最大化できる。
変数がメモリのどこに保存され、どう動いているのかが頭の中でビジュアル化できるようになると、エラーが出たときも「あ、ここは参照が共有されているな」「ここでメモリリークが起きているな」と原因が一発で特定できるようになります。
ここをクリアしたあなたなら、もうJavaScriptのメモリ管理の基本はバッチリマスターできていますよ!自信を持って、さらに深いコードの世界へ進んでいきましょう。