こんにちは!普段はフロントエンドからNode.jsの深層、さらにはV8エンジンのソースコードまでを行き来しながら、日々JavaScriptのパフォーマンスと向き合っているシニアアーキテクトです。
今回は、JavaScriptの基本中の基本である「変数の宣言(`var` / `let` / `const`)とメモリの仕組み」についてお話しします。
「変数を宣言するだけでしょ? `var`を`let`に変えるだけで動くよ」と思っていませんか?
実はここを深く理解できるかどうかが、プログラミング初学者から「真にJavaScriptを掌握したエンジニア」へステップアップできるかどうかの分かれ道なんです。
今回は、V8エンジン(JavaScriptの実行エンジン)が裏側でメモリをどう扱い、なぜ`let`や俳優たちがモダンな開発で必須なのかを、低レイヤの視点から優しく解き明かしていきますね。ここをクリアすれば、JavaScriptのメモリ管理の基本はバッチリマスターできますよ!
—
1. 変数って、そもそもメモリのどこに保存されているの?
私たちがコード中で `let x = 10;` と書いたとき、パソコンのメモリ(RAM)の中では何が起きているでしょうか?
JavaScriptの実行エンジンであるV8は、変数の値や構造を管理するために、主に「スタック領域(Stack)」と「ヒープ領域(Heap)」という2つの異なるメモリ空間を使い分けています。
- スタック領域:
- 机の上のメモ用紙のような場所です。サイズが固定されていて、関数が呼ばれたら積み上げられ、終わったら瞬時に消えます。処理速度が極めて高速です。
- ここには、数値や真偽値などの「プリミティブ型(値そのもの)」や、オブジェクトへの「参照(アドレス)」が置かれます。
- ヒープ領域:
- 巨大な倉庫のような場所です。サイズが可変で、オブジェクトや配列など、どれくらいメモリを食うか分からない大きなデータをポンと置いておく場所です。
- スタックにある変数は、このヒープ倉庫の「どこに荷物があるか」という住所(ポインタ)を握りしめています。
この基本を押さえた上で、`var`と`let`がメモリ上でどのような挙動の違いを生むのかを見ていきましょう。
—
2. `var`の正体:巻き上げ(Hoisting)とメモリの緩慢な関係
まずは、昔ながらの `var` についてです。`var` は、私たちがコードを書く上で色々な「ゆるさ」を持っています。その代表が巻き上げ(Hoisting)です。
以下のコードを見てください。
// varを使ったお馴染みのコード
console.log(greeting); // エラーにならず、undefinedと表示される!
var greeting = “こんにちは、V8世界へようこそ!”;
console.log(greeting); // “こんにちは、V8世界へようこそ!”
「あれ? 宣言する前に`console.log`しているのに、なんでエラーにならないの?」って思いますよね。
V8の裏側の動き:`var`のメモリ確保
V8エンジンは、コードを実行する前に「コンパイル(解析)」のフェーズを行います。
`var`で宣言された変数は、この段階でスコープの最上部に「巻き上げ」られ、メモリ上にその箱(変数名)が確保されます。
さらに、`var`の場合は同時に「初期値として `undefined`」がメモリに書き込まれます。
だから、宣言の前にコードからアクセスしても、エラーにならずに `undefined` が返ってくるのです。この「なんでも許してしまう仕様」が、大規模なアプリケーション開発で予期せぬバグ(変数の上書きなど)を生む原因になっていました。
—
3. `let`と`const`の決定的な違い:TDZ(一時的死領域)の真実
モダンなJavaScriptでは、`var`ではなく `let` や `const` を使うことが絶対のルールになっています。では、`let` はメモリやエンジンの挙動において何が違うのでしょうか?
// letを使ったコード
try {
console.log(message); // ここでReferenceError(参照エラー)が発生する!
} catch (e) {
console.log(e.message); // “Cannot access ‘message’ before initialization”
}
let message = “letは安全です”;
console.log(message); // “letは安全です”
`let` でも巻き上げ(変数の存在の認知)自体は行われます。しかし、`var` との決定的な違いは、「メモリ上に箱は作るけれど、初期値(`undefined`)を入れない」という点です。
一時的死領域(TDZ: Temporal Dead Zone)とは?
コードが実際にその行に到達して `let message = …` が実行されるまでの間、その変数はTDZ(一時的死領域)という厳重な警戒エリアに置かれます。
V8エンジンの視点で見ると、このエリアにある変数にアクセスしようとすると、エンジンが「おいおい、まだ初期化されてない危険なメモリ領域に触ろうとするな!」と検知し、強制的にエラー(`ReferenceError`)を投げます。
これにより、「初期化される前の変数にうっかりアクセスしてしまうバグ」を未然に防ぐことができるのです。これが、`let` が `var` よりも圧倒的に堅牢でモダンである理由です。
—
4. 低レイヤ視点:クロージャとヒープ退避のメカニズム
さて、ここからが少しディープな、チーフアーキテクトとしての本領発揮です。
「変数って、関数が終わったらスタックから消えるよね?」と思いますよね。ですが、JavaScriptにはクロージャ(Closure)という強力な仕組みがあります。
以下のコードをV8のメモリの動きと一緒に脳内トレースしてみましょう。
function createCounter() {
let count = 0; // スタックに積まれそうに見える変数
return function() {
count++; // 内部関数から外側の変数にアクセスしている(クロージャ)
return count;
};
}
const counter =orges = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
普通なら、`createCounter()` の実行が終了した時点で、その中のローカル変数 `count` が入っていたスタックフレームは破棄されるはずです。もし破棄されたら、2回目の `counter()` 呼び出しで `count` は消えてしまっているはずですよね。
しかし、実際にはちゃんとカウントアップされて `2` が出力されます。
なぜ `count` は消えないのか?(V8の最適化とヒープ退避)
V8エンジンは、コードを解析する中で「おっと、この変数 `count` は、関数が終わったあとも内側の無名関数から参照され続けるぞ(エスケープしている)」ということを検知します。
この瞬間、V8は頭を切り替えます。
「この `count` は通常のスタック領域に置いておくと関数終了時に消えてしまう。よし、ヒープ領域に退避させよう!」
V8の内部解析(IgnitionやTurboFanといったコンパイラ群)によって、スタックに確保されるはずだった変数が、生存期間を引き延ばすために自動的にヒープ領域へ昇格(あるいはヒープ上にクロージャ用のコンテキストとして保存)されるのです。
私たちが何気なく書いているクロージャの裏側では、V8エンジンがメモリの配置場所を動的に判断し、ガベージコレクション(GC)の管理下に置いてメモリリークが起きないよう細心の注意を払ってくれています。
—
まとめ:今日から使えるメモリ感覚
いかがでしたでしょうか? `var` と `let` の違いは、単なる「書き方の好み」ではありません。
1. `var` は巻き上げられて勝手に `undefined` で初期化される、少しルーズな子。
2. `let` / `const` はTDZ(一時的死領域)を導入し、初期化前の安全性を担保する厳格な子。
3. クロージャなどを使うと、V8エンジンは賢くメモリ(スタックからヒープ)を動的かつ効率的に管理してくれる。
この低レイヤのメモリレイアウトの仕組みを頭の片隅に置いておくだけで、コードを書くときの「お行儀」や、パフォーマンスを意識した設計ができるようになります。
「ここをクリアすれば、JavaScriptの基本はバッチリマスターできますよ!」
ぜひ、今日のコーディングから変数のスコープとメモリの動きを少しだけ意識してみてください。あなたの描くJavaScriptコードは、より美しく、より強靭なものに生まれ変わるはずです。それでは、また次回の深層でお会いしましょう!