【テクニカル・上級編】varの巻き上げとTDZの正体:JavaScriptエンジンがコードを解釈する2つのフェーズ – JavaScriptコア文法・モダン言語仕様と変数・関数解析バイブル

varの巻き上げとTDZの正体:JavaScriptエンジンがコードを解釈する2つのフェーズ

JavaScriptエンジンの内部で何が起きているのか。その問いに向き合わずして、現代の大規模フロントエンドや高スループットなNode.jsバックエンドのアーキテクチャを語ることはできない。

世の多くのチュートリアルは「`var`は巻き上げられるが、`let`や`const`はTDZ(Temporal Dead Zone:一時的死活領域)があるため巻き上げられない」という表層的な説明で筆を置く。しかし、V8をはじめとするモダンJSエンジン(SpiderMonkeyやJavaScriptCoreを含む)のソースコード、とりわけTC39の仕様策定とECMAScript仕様書(ECMA-262)の厳密なセマンティクスを紐解けば、`let`や`const`も例外なく巻き上げ(Hoisting)が行われているという厳然たる事実に行き当たる。

では、なぜ「宣言前のアクセス」において、`var`は`undefined`を返し、`let`/`const`は`ReferenceError`を投げるのか。この挙動の差異を、V8エンジンのコンパイルフェーズ(解析とバイトコード生成)から実行フェーズ(JITコンパイルとヒープ・スタック操作)に至るメモリレイヤの挙動から解き明かしていく。

—

1. V8エンジンがコードを解釈する2つのフェーズ

JavaScriptは「インタプリタ言語である」という古い常識は、V8の登場(2008年)とともに過去のものとなった。V8は、ソースコードを実行する前に、厳密な2つの主要フェーズを経る。

1. コンパイルフェーズ(Compilation Phase / 構文解析とスコープ割り当て)
2. 実行フェーズ(Execution Phase / バイトコードの解釈とJIT最適化)

コンパイルフェーズの内部動作

エンジンにスクリプトが渡された瞬間、Ignition(インタプリタ)へと送られる前段階として、Parserがソースコードを抽象構文木(AST:Abstract Syntax Tree)へと変換する。このAST構築の過程で、スコープ(Scope)の分析が静的に行われる。

ここで重要なのは、変数や関数の宣言が、そのスコープの先頭に「登録」されるという点だ。これが世に言う「巻き上げ(Hoisting)」の物理的な正体である。エンジンはコードの物理的な行数に関係なく、どの変数名がどのスコープに属しているかをコンパイル時にすべて把握し、環境レコード(Environment Record)へスロット(メモリ上の位置)を割り当てる。

—

2. `var`の巻き上げとメモリ初期化のメカニズム

`var`で宣言された変数は、コンパイルフェーズおよびスコープに入った瞬間の環境レコード生成フェーズにおいて、自動的に `undefined` で初期化される。

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

console.log(x); // 出力: undefined (エラーにならない)
var x = 42;
console.log(x); // 出力: 42

このとき、V8のヒープ上(あるいはスコープチェーン上のコンテキスト)では何が起きているのか。

1. コンパイル時: スコープ内に識別子 `x` が登録される。
2. 実行時(変数宣言の行に到達する前): 変数 `x` のメモリ領域には、即座に特殊なプリミティブ値である `undefined`(またはそれに類する初期化ポインタ)が書き込まれる。
3. 実行時(代入文の到達時): メモリ領域のポインタ、あるいは値が `42` に書き換わる。

この「宣言と同時にメモリが `undefined` で初期化される」という仕様こそが、`var`が持つ設計上の最大の脆弱性(意図しないグローバル汚染や巻き上げによるバグ)の根源である。

—

3. TDZ(一時的死活領域)の正体と `let`/`const` のメモリ管理

では、なぜ `let` と `const` は宣言前にアクセスすると `ReferenceError` になるのか。仕様上、`let` や `const` も巻き上げは発生している。コンパイルフェーズにおいて、それらの識別子は確実にスコープに登録されている。

決定的な違いは、「メモリの初期化タイミング」にある。

{
// — ここからTDZ —
// console.log(y); // ReferenceError: Cannot access ‘y’ before initialization

let y = 100; // — ここで初期化が完了し、TDZが終了 —

console.log(y); // 出力: 100
}

エンジン内部の挙動:未初期化状態(Uninitialized)

ECMAScript仕様書において、`let` や `const` の環境レコードは、コード上の宣言文に到達するまで 「Uninitialized(未初期化)」 という特別な状態としてマークされる。

  • コンパイルフェーズ: スコープに `y` が登録されるが、メモリ上のスロットには値も、`undefined` すら書き込まれない(あるいはアクセス禁止のフラグが立つ)。
  • TDZの期間: スコープに入ってから、実際の `let y = 100;` の評価文が実行されるまでの空間が TDZ(Temporal Dead Zone) である。この期間中に識別子にアクセスしようとすると、V8のランタイムチェックにより「未初期化状態へのアクセス」と判定され、即座に `ReferenceError` がスローされる。
  • 初期化文の通過時: エンジンが `let y = 100;` の評価に到達した瞬間、初めてメモリが初期化され、TDZが解除される。

この厳格な制約により、開発者は「値が定まっていない変数への意図しないアクセス」をランタイムレベルで強制的に排除できるようになった。これが近代的な堅牢なコードベースを支える低レイヤの防壁である。

—

4. 高度な応用:V8の隠しクラス(Hidden Classes / Shapes)と最適化

変数宣言のセマンティクスは、V8のJITコンパイラ(TurboFan)による最適化プロセスにも深く関わっている。

V8は動的型付き言語であるJavaScriptを高速化するため、オブジェクトのプロパティ構造を追跡する 「隠しクラス(Hidden Classes / 推奨用語: Shapes)」 を内部で生成する。

function createUser(name, age) {
this.name = name; // プロパティ追加によりShapeが遷移
this.age = age; // さらに別のShapeへ遷移
}

const user1 = createUser(‘Alice’, 30);
const user2 = createUser(‘Bob’, 25);

ここで `var` を多用し、スコープが曖昧で変数の型が実行中に激しく変動するコードを書くと、V8のインラインキャッシュ(IC:Inline Caching)が「メガモーフィック(Megamorphic)」状態に陥り、最適化が完全に外れる。

一方、`const` を用いて変数のイミュータビリティを保証し、スコープをブロックレベル(Lexical Scoping)で完全に隔離することは、V8のEscape Analysis(エスケープ解析)を助け、オブジェクトをヒープではなくスタック上に割り当てる(Stack Allocation)最適化を引き起こす契機となる。これにより、GC(ガベージコレクション)のプレッシャーを劇的に軽減できるのだ。

—

5. セキュリティ・サプライチェーンの文脈:TDZとプロトタイプ汚染の防壁

最後に、セキュリティ研究者やチーフアーキテクトの視点から、この変数スコープと巻き上げのメカニズムがどのようにセキュリティインシデントに関係するかを論じる。

近年のnpmサプライチェーン攻撃において猛威を振るう プロトタイプ汚染(Prototype Pollution) や、非安全なオブジェクトのマージ処理(Deep Merge等)を悪用したリモートコード実行(RCE)の多くは、動的なプロパティ代入の緩さと、グローバルまたは関数スコープ汚染(`var`の乱用による意図しないスコープ共有)の隙を突いている。

// 脆弱なパターンの例(グローバル汚染を誘発するvarの悪用)
var config = { debug: false };

function loadConfig(userProvidedData) {
// 意図しないグローバル変数の書き換えや、古いvarの巻き上げ挙動を利用したスコープハック
for (var key in userProvidedData) {
config[key] = userProvidedData[key];
}
}

もし、このコードで `var` の代わりに `const` および厳密なブロック文脈によるレキシカルスコープ、そして `Object.freeze()` やプロトタイプの凍結を徹底していれば、攻撃者がオブジェクトのプロトタイプチェーンを遡ってランタイムの挙動を乗っ取ることは極めて困難になる。

TDZや `const` によるイミュータビリティの強制は、単なる「書き換えミスを防ぐための構文糖衣」ではない。それは、V8ランタイムのメモリ空間における不正な状態遷移を防ぐための、最もプリミティブで強力なセキュリティ防壁なのだ。

—

結言

JavaScriptの変数の挙動を「そういう仕様だから」と片付けるのは、ドライバの構造を知らずにアクセルを踏むようなものだ。

`var` の巻き上げによる `undefined` の自動代入という過去の遺産と、`let`/`const` が導入したTDZという厳格なメモリ初期化ステート。この2つの違いをV8のコンパイルフェーズと実行フェーズの文脈から正確に理解しコードを構築することこそが、真にセキュアでハイパフォーマンスなモダンWeb/Node.jsアプリケーションをエンジニアリングするための絶対条件である。

タイトルとURLをコピーしました