【実務・中級編】V8エンジンのEnvironment Record:letとconstがスタック上に確保される仕組みとvarとのメモリレイアウト比較 – JavaScriptコア文法・モダン言語仕様と変数・関数解析バイブル

変数宣言の裏側で何が起きているのか? V8エンジンが暴く `var` と `let`/`const` のメモリレイアウトの真実

コードレビューをしていると、未だに「`var`を使っても動くし、何が違うのか分からない」というエンジニアに遭遇する。しかし、テクニカルリードとして断言しよう。`var` から `let` や `const` への移行は、単なる「モダンな書き方への流行り」ではない。それは、V8エンジンのメモリ管理機構、ひいてはアプリケーションのパフォーマンスとメモリ安全性に直結する極めて重要なアーキテクチャ上の選択なのだ。

今回は、JavaScriptのコアランタイムである V8エンジンが、変数の宣言方法によっていかにメモリレイアウトを変化させ、スコープを管理しているのか、その深層を解き明かしていく。

—

1. `var` の正体:関数スコープとヒープアロケーションの代償

まず、古の亡霊である `var` の挙動からおさらいしよう。`var` は「関数スコープ」を持ち、宣言がスコープの先頭に巻き上げられる(Hoisting)ことは周知の通りだ。しかし、この「巻き上げ」と「初期値としての `undefined` の代入」が、V8エンジンのメモリ管理においてどのような負荷を生んでいるかを意識したことはあるだろうか?

`var` で宣言された変数は、しばしばコンテキスト(Context)と呼ばれるヒープ上のオブジェクト領域に割り当てられる。

function legacyFunction() {
var heavyData = new Array(1000000).fill(‘💩’);
if (true) {
var leakedVar = ‘global-ish in function scope’;
}
console.log(leakedVar); // ブロックの外からアクセスできてしまう
}

このコードにおいて、`heavyData` も `leakedVar` も、同一の関数実行コンテキスト(Function Execution Context)に紐づく変数オブジェクト(Variable Object / Context)の一部としてヒープ上に確保される。
V8のガベージコレクタ(GC)の観点から見ると、関数スコープを持つ `var` は、その関数がスコープアウトするまで、たとえ不要になったブロックを抜けてもメモリ上に残り続ける。さらに、クロージャが絡むと、`var` で宣言された変数は容赦なくヒープ上の Context にキャプチャされ、メモリリークの温床となる。

—

2. `let` と `const` の核心:Environment Record と最適化されたストレージ

これに対し、ES2015で導入された `let` と `const` は、JavaScriptに「ブロックレベルスコープ」をもたらしただけでなく、V8エンジンの最適化パイプライン(Ignition と TurboFan)におけるメモリレイアウトを劇的に変えた。

V8では、スコープ内の変数は `Environment Record`(環境レコード)という内部データ構造によって管理される。`let` や `const` で宣言された変数は、必ずしもヒープ上の重いオブジェクトとして確保されるとは限らない。

スタック領域および最適化されたレジストリ・スコープへの配置

V8のエグゼキューションにおいて、ブロック(`{}`)内で完結する `let` や `const` は、関数のローカル変数としてスタックフレーム内、あるいはコンパイラが型と生存期間(Life-cycle)を完全に追跡できる最適化されたスコープ領域に直接マッピングされる。

これにより、以下のメリットが生じる。
1. 生存期間の最小化(Temporal Locality): ブロックを抜けた瞬間、その変数が占有していた領域は即座に解放(あるいは上書き可能に)される。GCの走査コストを劇的に下げる。
2. 一時的死帯(TDZ: Temporal Dead Zone)による安全性: `var` のような「巻き上げによる `undefined` の混入」を防ぎ、初期化前のアクセスをランタイムレベルでコンパイルエラー(あるいはReferenceError)として検知する。

—

3. 実務で直面するパフォーマンスとメモリの罠:クロージャとブロックスコープ

ここで、実際のプロダクションコードを想定した設計上の注意点を見てみよう。非同期処理やイベントリスナーのループ処理で、しばしば見落とされるメモリ効率の差だ。

以下のコードを比較してほしい。

【アンチパターン】`var` と不適切なスコープによるメモリの延命

// 悪い例:メモリ効率が悪く、意図しないスコープ共有が起きる
function processLegacyBatch(items) {
var results = [];
for (var i = 0; i < items.length; i++) { var item = items[i]; results.push(function() { return item.process(); // すべてのクロージャが同じスコープの `item` を参照してしまう }); } return results; }

  • 何が問題か: `var i` と `var item` は関数スコープ全体で共有されるため、ループごとに新しいバインドが作られない。さらに、変数がヒープ上のコンテキストに退避するため、GCの回収対象になりにくく、メモリフットプリントが肥大化する。

【プロダクション・ベストプラクティス】`const` とブロッククタースコープの極致

/