こんにちは!普段はフロントエンドのパフォーマンスチューニングやNode.jsのバックエンド設計で四苦八苦しながらも、JavaScriptの奥深さに魅了されているシニアエンジニアです。
皆さんは普段何気なく `var` や `let`、`const` を使って変数を宣言していますよね。「とりあえず `const` を使っておけば安全」「昔は `var` だったらしい」という認識でコードを書いている方も多いのではないでしょうか。
でも、ここからがJavaScriptを本当に手懐けるための分かれ道です。「なぜ `var` だとバグりやすくて、`let` や `const` だと安全なのか」。その理由は、単なる文法のルールではなく、JavaScriptエンジン(特に私たちが普段お世話になっているGoogle ChromeやNode.jsの心臓部であるV8エンジン)が、メモリ上でどのように変数を管理しているかという「低レイヤの仕組み」に直結しています。
今回は、V8エンジンの内部構造(Environment Record)を覗き見しながら、`let` と `var` のメモリ確保の決定的な違いを、一緒に紐解いていきましょう!ここをクリアすれば、あなたのJavaScriptのコードを見る目は劇的に変わりますよ。
—
1. 変数宣言の基本のおさらい:何がどう違うの?
まずは、初心者の方でも迷わないように、それぞれの基本的な振る舞いをおさらいしておきましょう。
JavaScriptには変数を宣言する方法として、歴史的な `var` と、モダンな `let` / `const` があります。
// var の世界:昔ながらの宣言
console.log(oldStyle); // 意外!エラーにならず undefined と表示される
var oldStyle = “こんにちは”;
// let の世界:モダンな宣言
try {
console.log(modernStyle); // ここでエラー(ReferenceError)が発生する!
} catch (e) {
console.log(e.message); // Cannot access ‘modernStyle’ before initialization
}
let modernStyle = “はじめまして”;
この違い、不思議に思ったことはありませんか?
`var` は宣言する前に使っても `undefined` になるだけで怒られないのに、`let` は「初期化する前にアクセスできない」と怒られてしまいます。これが有名な「巻き上げ(Hoisting)」の挙動の違いですね。
では、なぜこのような違いが生まれるのでしょうか?それを知るために、V8エンジンの頭の中を覗きに行きましょう。
—
2. V8エンジンの頭の中:Environment Record とメモリの正体
JavaScriptのコードが実行されるとき、V8エンジンは変数を管理するために「Environment Record(環境レコード)」という内部データ構造を作ります。いわば、変数の住所録のようなものです。
コードが実行される直前(コンパイル段階・解析段階)、V8はこの住所録に「どんな変数があるか」をあらかじめ登録します。これが「巻き上げ」の物理的な正体です。しかし、「住所録に名前を書くタイミング」と「実際にメモリを割り当てて使えるようにするタイミング」が、`var` と `let` で全く異なるのです。
var の場合:宣言と同時に「初期化(undefined)」まで終わらせる
`var` で宣言された変数は、V8がコードを読み込むスキャン段階で、Environment Recordに「この変数あるよ」と登録されると同時に、自動的にメモリ上にスロットが確保され、初期値として `undefined` が放り込まれます。
そのため、コードの見た目上で宣言行に到達する前にその変数を呼び出しても、V8は「あ、`undefined` が入ってるスロットね、はいどうぞ」と優しく返してくれます。これがバグの温床になる原因です。
let / const の場合:登録はするが、メモリの割り当ては「後回し」
一方、`let` や `const` は違います。
V8はコードの解析段階でEnvironment Recordに変数の名前を登録はしますが、実際のメモリの初期化は行いません。
コードの実行が実際にその `let` の行に到達するまでの間、その変数は「TDZ(Temporal Dead Zone:一時的死者ゾーン / 時間的デッドゾーン)」という特殊な状態に置かれます。このゾーンにいる変数に触れようとすると、V8は「まだメモリが初期化されてないから危ない!」と `ReferenceError` をビシッと投げつけてくれるのです。
—
3. 低レイヤの視点:スタックとヒープ、そして最適化の真実
ここからが少しディープな、プロフェッショナルのお話です。V8エンジンは、メモリ効率を極限まで高めるために、変数をスタック(Stack)やヒープ(Heap)というメモリ領域に配置します。
スコープとメモリの配置
- スタックメモリ: 関数が呼び出されたときのローカル変数や実行コンテキストが積まれる、高速で読み書きできる領域。
- ヒープメモリ: クロージャ(関数が作られた環境を記憶する仕組み)などで参照され、関数が終了しても生き残り続ける必要があるオブジェクトや変数が置かれる領域。
`var` は関数スコープを持ちます。そのため、意図しない場所から同じ変数名でアクセスできてしまったり、関数全体を通じてメモリ上に存在し続けようとしたりします(これがメモリリークや意図しない上書きの元凶になります)。
一方、`let` と `const` はブロックレベルスコープ(`{}` の中だけ)を持ちます。
V8エンジンは、ブロックが終了した瞬間にその変数がもう使われないことを確信できるため、スタック上のメモリを即座に解放したり、最適化(インラインキャッシュやレジスタ割り当ての効率化)を非常にアグレッシブに行うことができます。
function processOrder() {
// block 1
{
let tempId = generateId(); // このブロックだけで消費されるメモリ
validate(tempId);
} // <-- ここを抜けた瞬間、tempId のメモリ領域はV8によって迅速に回収対象(または再利用)になる
// var だと関数全体に影響が及び、V8の最適化エンジンが「この変数はどこまで生存させるべきか」の追跡にコストを払わされることになります
}
V8エンジンのJIT(Just-In-Time)コンパイラである TurboFan がコードを機械語にコンパイルする際、変数のライフサイクル(生存期間)が明確で、スコープが狭い (`let` / `const` が使われている) ほうが、型推論やメモリの静的解析が圧倒的にしやすくなります。つまり、`let` や `const` を使うことは、V8のエンジンパフォーマンスを直接的に引き上げることに繋がっているのです。
—
4. 陥りやすい罠:なぜ `var` を今すぐ駆逐すべきなのか
ここで、実務の現場でよくある失敗例を見てみましょう。`var` の仕様を理解していないと、次のようなバグに頭を悩ませることになります。
// 【やってはいけないアンチパターン】
function printIndices() {
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(`インデックス: ${i}`);
}, 100);
}
}
printIndices();
// 期待値: 0, 1, 2 と表示されてほしい
// 実際の出力: 3, 3, 3 と表示されてしまう!
なぜこうなるのか?
`var` は関数スコープです。そのため、`for` ループの中で宣言された `i` は、ループの外(関数内)から見るとただ一つの同じ変数を指しています。
`setTimeout` のコールバック関数が実行される頃には、すでにループが終了して `i` の値は `3` になりきっています。結果として、すべてのコールバックが「3」を参照してしまうのです。
これを解決するために、かつては即時実行関数(IIFE)などを使ってスコープを無理やり作っていました。しかし、モダンなJavaScriptなら、`let` に書き換えるだけで一発解決です。
// 【モダンで正しい書き方】
function printIndicesSafe() {
for (let i = 0; i < 3; i++) { // let はブロック(ループの各繰り返し)ごとに新しいスコープとメモリの束縛を作る
setTimeout(function() {
console.log(`インデックス: ${i}`);
}, 100);
}
}
printIndicesSafe();
// 実際の出力: 0, 1, 2 が綺麗に表示される!
`let` をループの初期化式で使うと、V8はループのイテレーション(繰り返し)ごとに全く別の変数のインスタンス(Environment Recordの別スロット)を生成します。そのため、それぞれの非同期処理が「その瞬間どこのメモリを指していたか」を正しく保持できるのです。
—
まとめ:明日から使える「変数の極意」
今回は、V8エンジンのEnvironment Recordやメモリ管理という低レイヤの視点から、`let` と `var` の決定的な違いを解説しました。
- `var` は避けるべき理由:
- 宣言より前でも `undefined` でアクセスできてしまう(巻き上げの弊害)。
- 関数スコープのためライフサイクルが曖昧になり、V8のメモリ最適化の邪魔になる。
- ループ等の非同期処理で意図しないスコープ汚染(バグ)を引き起こしやすい。
- `let` / `const` を使うべき理由:
- TDZ(一時的死者ゾーン)により、バグのあるコードを早期にエラーとして検知できる。
- ブロックレベルスコープにより、V8エンジンがメモリを効率よく管理・解放できる。
- コードの意図が明確になり、パフォーマンスもエンジン最適化の恩恵を受けられる。
「たかが変数の宣言」と侮るなかれ。こうした細かな文法の選択の積み重ねが、ブラウザのレンダリングを滑らかにし、Node.jsサーバーのメモリ消費量を抑える最高峰のコードを作り上げます。
ここをクリアしたあなたなら、もうJavaScriptの変数やスコープで迷うことはありません。自信を持って、明日からのコードをすべて `const` と `let` で美しく書き換えていきましょう!