こんにちは!JavaScriptの奥深い世界へようこそ。フロントエンドからNode.jsのバックエンドまで、日々JavaScriptのコードを書き、その裏側でうごめくV8エンジンの挙動に思いを馳せているシニアアーキテクトの私です。
今回は、JavaScriptの学習者が必ず一度はハマる「変数の巻き上げ(Hoisting)」という現象について、徹底的に深掘りしていきましょう。
「変数を宣言する前に使ってなぜかエラーにならない」「`let`や`const`だとなぜかエラーになる」——そんな不思議な現象の裏側で、V8エンジンやJavaScriptランタイムがメモリ空間で何を行っているのか。AST(抽象構文木)の生成から、見えないメモリ領域である「Environment Record(環境レコード)」への登録まで、そのメカニズムを優しく、かつ本質的に解き明かしていきます。
ここをクリアすれば、JavaScriptの変数スコープの基本はバッチリマスターできますよ。それでは、さっそくエンジンの内部世界へ飛び込んでみましょう!
—
1. 変数の巻き上げって、そもそも何が起きているの?
JavaScriptのコードは、私たちが書いた瞬間そのままブラウザやNode.jsで実行されているわけではありません。実は、コードが実行される直前に「Creation Phase(生成フェーズ)」という重要な下準備の時間が存在します。
この生成フェーズにおいて、JavaScriptエンジンはコード全体をざっとスキャンし、変数や関数の宣言をコードの先頭に「引き上げた(巻き上げた)」かのように扱ってメモリ上に登録します。これが「巻き上げ(Hoisting)」と呼ばれる現象の正体です。
言葉だけだと少し抽象的なので、まずは典型的なコードを見てみましょう。
// 宣言より前なのに呼び出せる!?
console.log(greeting); // 出力: undefined (エラーにならない!)
var greeting = “こんにちは、V8エンジン!”;
console.log(greeting); // 出力: こんにちは、V8エンジン!
他のプログラミング言語(JavaやC#など)を触ったことがある方なら、「えっ、宣言する前に変数を使ったら `ReferenceError`(参照エラー)になるはずでは……?」と驚かれるのではないでしょうか。
しかし、JavaScriptの `var` ではエラーになりません。最初の `console.log(greeting)` は `undefined` を返します。なぜこのような挙動になるのか、その裏側のメカニズムを覗いてみましょう。
—
2. V8エンジンの頭の中:ASTからEnvironment Recordへの登録まで
JavaScriptエンジン(ここではChromeやNode.jsで使われているGoogleのV8をイメージしてください)が、ソースコードを実行するまでのプロセスは、ざっくり言うと次のような流れになっています。
1. 字句解析・構文解析(Parsing & AST Generation):
ソースコードを読み込み、人間が書いたテキストをコンピュータが理解できる木構造のデータ、すなわちAST(抽象構文木:Abstract Syntax Tree)へと変換します。
2. 生成フェーズ(Creation Phase):
ASTを元にして、変数や関数を保管するためのメモリ空間であるEnvironment Record(環境レコード)を作成します。ここで「巻き上げ」が発生します。
3. 実行フェーズ(Execution Phase):
コードを上から順に実際に実行し、値の代入や関数の呼び出しを行っていきます。
`var` の場合:メモリ確保と同時に `undefined` で初期化される
生成フェーズの時点で、V8エンジンはコード内にある `var greeting` を見つけると、Environment Recordに対して「`greeting` という変数の場所を確保しなさい」と指示を出します。
さらに、`var` の場合は親切(あるいは曲者)なことに、メモリの確保と同時に初期値として `undefined` を自動的に詰め込みます。
そのため、実行フェーズでコードが上から読まれて `console.log(greeting)` に到達したときには、すでにメモリ上に `greeting` が存在し、中身が `undefined` であるため、エラーにならずにそのまま出力されたというわけです。
—
3. 近代JavaScriptの救世主:`let` と `const` と「一時的死神領域」
「じゃあ、変数が勝手に `undefined` で初期化されるのはバグの温床になりそうだな……」と思われましたよね。その通りです!この曖昧さを解決し、より堅牢なコードを書くために、ES2015(ES6)で導入されたのが `let` と `const` です。
`let` や `const` でも、実は「巻き上げ」自体は起きています。しかし、`var` とは決定的な違いがあります。
// let の場合
console.log(message); // 💥 ReferenceError: Cannot access ‘message’ before initialization
let message = “モダンなJavaScriptの世界へようこそ!”;
今度はしっかりとエラー(参照エラー)になりましたね。一体何が起きているのでしょうか?
Temporal Dead Zone(一時的死神領域:TDZ)の正体
`let` や `const` でも生成フェーズでEnvironment Recordへの登録(=巻き上げ)は行われます。しかし、`var` と違って `undefined` での自動初期化が行われません。
この「変数はメモリに登録されているけれど、まだ値が初期化されていない(コード上の宣言文に到達していない)」状態の空間・期間のことを、TDZ(Temporal Dead Zone:一時的死神領域)と呼びます。
【実行前の生成フェーズ】
[ Environment Record ]
- greeting (var) -> 内存確保 + `undefined` で初期化完了!
- message (let) -> 内存確保 + 初期化は「まだ」行われない(ここがTDZ!)
【実行フェーズ】
console.log(message); -> ❌ TDZのエリアにいるためアクセス拒否(ReferenceError)
let message = “…”; -> ✨ ここを通過した瞬間に初期化され、TDZから脱出!
つまり、`let` や `const` でも巻き上げは起きていますが、「初期化される前にアクセスすると危険だから、エンジンが強制的にエラーを投げて守ってくれる」ようになっているのです。これが、モダンなJavaScriptにおける安全性の秘訣です。
—
4. 実務で役立つ!巻き上げにまつわる落とし穴と回避テクニック
現場の開発現場で、この巻き上げの仕組みを知らないと、意図しないバグに頭を抱えることになります。特に注意したいのが、「関数宣言」と「関数式」の挙動の違いです。
落とし穴:関数宣言 vs 関数式
関数も変数と同様に巻き上げの影響を受けますが、その書き方によって全く異なる挙動を示します。
// — パターンA:関数宣言(Function Declaration) —
// 驚くべきことに、定義する前でも呼び出せます!
sayHello(); // 出力: “こんにちは!”
function sayHello() {
console.log(“こんにちは!”);
}
関数宣言の場合、V8エンジンは生成フェーズで「関数の中身(本体)ごと」メモリに登録します。そのため、コードのどこからでも呼び出すことが可能です。
// — パターンB:関数式(Function Expression) —
// 変数に無名関数を代入するスタイル
sayHi(); // 💥 TypeError: sayHi is not a function (または ReferenceError)
var sayHi = function() {
console.log(“ハイ!”);
};
一方、変数(この場合は `var`)に関数を代入する「関数式」の場合、巻き上げられるのは変数名の `sayHi`(初期値は `undefined`)だけであり、関数の本体は代入されるまでメモリに入りません。そのため、`undefined()` を実行しようとしてタイプエラー(あるいは `let` の場合は参照エラー)になってしまいます。
ベストプラクティス:コードの意図を明確にする書き方
現場のシニアエンジニアとして、この挙動を踏まえた上で次のようなコーディング規約をおすすめします。
1. 変数は使う直前、あるいはスコープの最上部で `const` または `let` で宣言する
- `var` は原則として使用禁止(レガシーなコードベース以外では使う理由がありません)。
2. 変数を宣言する前にコード内で参照しない
- TDZ(一時的死神領域)の恩恵を受け、意図しない `undefined` のバグをコンパイル段階(実質的には実行時エラー)で防ぎましょう。
3. 関数は「関数宣言」で書くか、アロー関数(関数式)を必ず定義した後に呼び出す
- 処理の流れを上から下へ自然に読めるようにするため、コードの構造を整理整頓する意識を持ちましょう。
—
まとめ
今回は、V8エンジンが実行する「変数の巻き上げ」の裏側について、ASTからEnvironment Record、そしてTDZ(一時的死神領域)の概念までを深く解説しました。
- 巻き上げとは:コード実行前の「生成フェーズ」で変数や関数の登録が行われる現象のこと。
- `var` の場合:宣言と同時に `undefined` で初期化されるため、宣言前でもアクセスできてしまう。
- `let` / `const` の場合:登録はされるが初期化はされず、TDZ(一時的死神領域)によって安全に保護される。
このメカニズムが頭の片隅にスッと入っていれば、デバッグの際に「あ、今ここはTDZに引っかかっているな」「ここは巻き上げで `undefined` になっているな」と、脳内でV8エンジンの動きを完璧にトレースできるようになります。
ここをクリアしたあなたなら、JavaScriptの変数やスコープ周りで怖いものはありません!ぜひ日々のコーディングにこの知見を活かしてみてくださいね。それでは、また次回のモダンJavaScriptの旅でお会いしましょう!