【入門編】varの巻き上げをAST解析から紐解く:コンパイルフェーズで何が起きているのか – JavaScriptコア文法・モダン言語仕様と変数・関数解析バイブル

こんにちは!JavaScriptの世界へようこそ。
フロントエンドのUI構築から、Node.jsによるバックエンドの極限チューニングまで、日々コードと向き合っていると、「JavaScriptって、実は裏側でめちゃくちゃダイナミックなことやってるんだよな」と感動させられます。

今回は、JavaScriptの学習者が必ず一度はハマる「変数の巻き上げ(Hoisting)」、そしてその中でも特に古くから使われている `var` の挙動に焦点を当てます。「なぜコードを書く順番を変えていないのに、エラーにならずに `undefined` が返ってくるの?」という疑問を持ったことはありませんか?

ネットを検索すると「変数の宣言がコードの先頭に持ち上げられる現象です」というお決まりの説明が出てきますが、伝説のアーキテクトから言わせれば、「持ち上げられている(ホイスティング)」というのはあくまで人間の脳内を助けるための比喩に過ぎません。

実際には、V8などのJavaScriptエンジンがソースコードを読み解き、コンパイルフェーズ(実行前)で何を行っているのか。AST(抽象構文木)の視点も交えながら、優しく、そして本質的に紐解いていきましょう。ここをクリアすれば、JavaScriptのランタイム挙動の基本はバッチリマスターできますよ!

—

1. まずは基本のおさらい:`var` の奇妙な挙動

他のプログラミング言語(JavaやC#、Pythonなど)からやってきた開発者が、JavaScriptの `var` を触ると、まず間違いなく次のような混乱に陥ります。

// 変数宣言する前にアクセスしてみる
console.log(message); // エラーにならない!? 出力は “undefined”

var message = “こんにちは、JavaScriptの世界へ!”;

console.log(message); // “こんにちは、JavaScriptの世界へ!”

普通に考えたら、まだ `var message` が登場する前に `console.log(message)` を実行しているのだから、`ReferenceError`(そんな変数知らんよエラー)になりそうなものですよね。ですが、JavaScriptは怒るどころか、静かに `undefined`(値はまだ入っていないけど、変数の存在は知ってるよ)と返してきます。

これが、いわゆる「`var` の巻き上げ(Hoisting)」と呼ばれる現象です。
では、この裏側でJavaScriptエンジンの中では何が起きているのでしょうか?

—

2. エンジンの脳内を覗く:AST解析とコンパイルフェーズ

JavaScriptエンジン(V8など)は、私たちが書いたコードをいきなり上から順番に実行しているわけではありません。実は、コードを実行する前に、大きく分けて2つのフェーズを踏んでいます。

1. コンパイル(生成)フェーズ: コード全体をスキャンし、AST(抽象構文木)に変換しながら、変数や関数をあらかじめメモリ上の「お部屋(Environment Record:環境レコード)」に登録する。
2. 実行フェーズ: 上から順番にコードを実際に実行し、値の代入や関数の呼び出しを行う。

この「コンパイルフェーズ」の動きこそが、巻き上げの正体です。

AST(抽象構文木)とEnvironment Recordのイメージ

JavaScriptエンジンがソースコードをパース(解析)してASTを作るとき、`var message = “こんにちは…”` というコードは、木構造(ツリー)のノードに分解されます。

エンジンはコードを実行する前に、そのツリーをざっと見渡し、「お、`var` で宣言されている変数 `message` があるな。よし、今のうちにこのスコープの「お部屋(Environment Record)」に `message` という名前の枠を作っておこう」と登録作業を行います。

さらに、`var` の特権として、このコンパイルフェーズで初期値として自動的に `undefined` を割り当てておくという仕様があります。

[ コンパイルフェーズの脳内イメージ ]
スコープ(大部屋)のEnvironment Record:

  • message: undefined (← エンジンが勝手に用意してくれた!)

この準備がすべて終わったあとに、初めて「実行フェーズ」が始まります。だから、1行目の `console.log(message)` が実行されたときには、すでにエンジンは `message` の存在を知っており、`undefined` を取り出すことができるというわけです。

—

3. コードの実際の動きをトレースしてみよう

頭の中をエンジンに同期させて、次のコードの動きを完全にトレースしてみましょう。

// — コンパイルフェーズが完了した直後の状態 —
// Environment Record に { a: undefined, b: undefined } が登録されている

console.log(a); // 出力: undefined (aの箱はあるが、まだ値が入っていない)

var a = 100; // ここで初めて実行フェーズにおいて「100」が代入される

console.log(a); // 出力: 100

// 関数スコープにおける巻き上げの例
function showScore() {
// 関数内でも同様に、スコープの先頭で b が巻き上げられる
console.log(b); // 出力: undefined

var b = 50;

console.log(b); // 出力: 50
}

showScore();

このように、`var` は「宣言がスコープのトップに引っ張り上げられている」ように見えるだけで、実際には「エンジンがコード実行前にスコープ内のすべての `var` 宣言を検知して、メモリ(Environment Record)に枠を作り、初期値 `undefined` をブッロクしている」というのが正確なメカニズムです。

—

4. なぜ `var` はバグの温床になるのか?

ここまで読んで、「なんだ、エンジンが親切に準備してくれてるなら便利じゃん!」と思ったかもしれません。しかし、実際の開発現場では、この `var` の仕様が原因で、原因の特定が極めて困難なバグ(予期せぬ挙動)を何度も引き起こしてきました。

次のコードを見てください。

var userCount = 10;

function processUsers() {
// 開発者はここで新しいローカル変数を作ったつもりが…
// うっかり var をつけ忘れた、あるいは後から同じ名前の var を書いてしまったとする

if (userCount > 5) {
var userCount = 99; // ← ここで巻き上げが発生!
}

console.log(userCount); // 期待値: 10 なのか? それとも 99 なのか?
}

processUsers();

なんと、このコードの出力結果は `99` ではなく、`undefined` になります(厳密には `if` の中に入る前に、関数スコープのトップで `var userCount` が巻き上げられ、グローバルの `userCount` がシャドーイングされて `undefined` になるため)。

`var` は「関数スコープ」を持ちます。ブロック(`{}`)を無視してスコープの最上位まで巻き上げられてしまうため、意図しない変数の上書きや、スコープの汚染が簡単に起きてしまうのです。これが、モダンなJavaScriptにおいて `var` の使用が強く非推奨とされている最大の理由です。

—

5. 現代のモダンJavaScript:`let` と `const` がもたらした解決策

私たちが生きる現代のJavaScript(ES6以降)では、この `var` が抱える欠点を完全に克服するために、`let` と `const` が導入されました。

// let / const でも巻き上げ自体は起きる!
console.log(modernVar); // ReferenceError: Cannot access ‘modernVar’ before initialization

let modernVar = “私は安全です”;

「あれ?エラーになったぞ? 巻き上げ起きてないじゃん!」と思いましたか?
ここに大きな誤解があります。実は、`let` や `const` でもコンパイルフェーズにおける巻き上げ(Environment Recordへの登録)は発生しています。

決定的な違いは、「初期値(`undefined`)を自動的に代入するかどうか」です。

  • `var`: 登録時に自動で `undefined` を代入する(だからエラーにならない)。
  • `let` / `const`: 登録はするが、初期値は代入しない。コードがその宣言行に到達するまでの間、その変数は「Temporal Dead Zone(一時的死地:TDZ)」と呼ばれるアクセス禁止エリアに置かれる。

このTDZのおかげで、初期化される前に変数にアクセスしようとすると、JavaScriptエンジンが即座に `ReferenceError` を投げてバグを未然に防いでくれるようになりました。

—

まとめ:今日の知見を胸に、明日からのコードをよりセキュアに

いかがでしたでしょうか? 今回の学びを簡単にまとめておきます。

1. 巻き上げの正体: コードが実行される前の「コンパイルフェーズ」において、JavaScriptエンジンがEnvironment Recordに変数を登録するプロセスである。
2. `var` の挙動: 登録と同時に `undefined` が自動代入されるため、宣言前でも参照できてしまう(これがバグの温床)。
3. モダンなアプローチ: `let` や `const` はTDZ(一時的死地)を形成し、安全でないアクセスをコンパイル/ランタイムエラーで厳格に弾いてくれる。

JavaScriptのランタイムが裏側でどう動いているのかを知ると、コードを書くときの「確信」が変わってきます。エラーが出たときにも「あ、いまエンジンはEnvironment Recordのあそこのメモリ空間でつまずいてるな」と脳内トレースができるようになるはずです。

この基本をマスターすれば、もう `var` の気まぐれな挙動に怯える必要はありません。
自信を持って、モダンで堅牢なJavaScriptコードを書き上げていきましょう!

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