こんにちは!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コードを書き上げていきましょう!