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

こんにちは!日々の開発、本当にお疲れ様です。

JavaScriptを書き始めた頃、「あれ、変数を宣言したはずなのに、なぜか `undefined` になるぞ?」「いや、今度は `ReferenceError` で怒られたぞ……?」と首を傾げた経験はありませんか?

`var`、`let`、そして `const`。変数を作るための道具なのに、なぜこれほど挙動が違うのか不思議ですよね。ネットで調べると「巻き上げ(Hoisting)が原因です」「TDZ(一時的デッドゾーン)があります」といった専門用語が出てきて、余計に混乱してしまった方も多いのではないでしょうか。

大丈夫です。ここをクリアすれば、JavaScriptのランタイムが頭の中で完全に透けて見えるようになり、あなたのコードはワンランク上の確実なものになりますよ。今日は、JavaScriptエンジンがコードをどのように読み解いているのか、その裏側のドラマを一緒に覗いてみましょう。

—

JavaScriptエンジンは「2つのフェーズ」でコードを読む

私たちが書いたJavaScriptのコードは、ブラウザやNode.jsに渡された瞬間、一気に上から下へ実行されるわけではありません。実は、V8などのJavaScriptエンジンは、コードを効率よく安全に動かすために、「コンパイル(生成)フェーズ」と「実行フェーズ」という明確な2つのステップを踏んで処理しています。

イメージとしては、劇の舞台裏を想像してください。

1. コンパイルフェーズ(舞台裏の準備)
幕が上がる前、監督(エンジン)が台本をくまなくチェックし、「このシーンには誰が登場するのか」「小道具はどこに置くか」をあらかじめリストアップしてメモリ上にスペースを確保する作業です。ここで「変数名」が記憶されます。
2. 実行フェーズ(本番の舞台)
いよいよ幕が上がり、上から順にコードが実際に実行されていきます。代入(`=`)や関数の呼び出しが行われるのは、このフェーズに入ってからです。

この「準備」と「本番」のタイムラグこそが、巻き上げやTDZの正体を解く全ての鍵になります。

—

`var` の巻き上げ(Hoisting)の正体

まずは、古くからある `var` の世界を見てみましょう。`var` は、コンパイルフェーズでどのように扱われるのでしょうか。

次のコードを見てください。宣言する前に変数を呼んでいます。

console.log(message); // 何が出力されるでしょうか?
var message = ‘こんにちは、JavaScript!’;
console.log(message);

他のプログラミング言語(JavaやC#など)の感覚だと、「まだ宣言してないのにエラーにならないの?」と驚きますよね。しかし、JavaScriptでは最初の `console.log(message)` はエラーにならず、`undefined` と出力されます。

なぜこうなるのか、先ほどの2つのフェーズに当てはめてエンジンの脳内をトレースしてみましょう。

  • コンパイルフェーズでの動き

エンジンはコード全体をスキャンし、「お、`var message` があるな」と見つけます。そして、スコープ(変数の有効範囲)の先頭に、メモリ上の領域をこっそり確保します。さらに親切なことに、このとき `var` で宣言された変数には初期値として `undefined` を自動でブチ込んでおくのです。これが「変数の宣言がコードの先頭に持ち上げられた(=巻き上げられた)」ように見える正体です。

  • 実行フェーズでの動き

1回目の `console.log(message)` が実行された時点では、まだ `message` には `’こんにちは、JavaScript!’` という文字列は代入されていません。コンパイル時に仕込まれた `undefined` がそのまま読み出されるため、`undefined` が表示されます。その後、2行目で値が代入され、2回目のログで無事に文字列が表示されます。

つまり、`var` の巻き上げは、エンジンが事前にメモリを確保し、`undefined` で初期化してくれていたという裏側の優しさ(時にバグの原因になりますが……)だったのです。

—

`let` と `const` はなぜ「宣言前にアクセスできない」のか?

さて、ここからが現代のモダンなJavaScript(ES6以降)の話です。`let` や `const` でも、実は巻き上げ自体は起きています。

「えっ、じゃあなんで `let` はエラーになるの?」と思いますよね。実際にコードで確認してみましょう。

try {
console.log(secretKey); // ここでエラーが発生する
} catch (error) {
console.error(error.message); // ReferenceError: Cannot access ‘secretKey’ before initialization
}

let secretKey = ‘秘密のパスワード’;

`var` のときは `undefined` になりましたが、`let` や `const` で同じことをすると、`ReferenceError`(参照エラー)という手痛い洗礼を受けます。

この違いを生み出しているのが、TDZ(Temporal Dead Zone:一時的デッドゾーン)という概念です。

メモリ確保と「初期化」の断絶

`let` や `const` がコンパイルフェーズと実行フェーズでどう扱われるか、その内部の仕組みを覗いてみましょう。

1. コンパイルフェーズ
エンジンは `let secretKey` を見つけて、メモリ上に領域を確保します。ここまでは `var` と全く同じです。
2. 「初期化」が行われないTDZの誕生
しかし、ここが `var` との決定的な分水嶺です。エンジンは `let` や `const` の変数に対して、`undefined` による自動初期化を行いません。 メモリの領域は確保されたものの、中身が空っぽの、触ってはいけない「立ち入り禁止エリア(TDZ)」としてマークされます。
3. 実行フェーズ(宣言文に到達するまで)
コードが上から実行され、実際に `let secretKey = …` の行に到達するまでの間、その変数のメモリ空間はTDZの中にあります。このゾーンにいる変数にアクセスしようとすると、エンジンは「おいおい、まだ初期化されてない危険な領域に触ろうとするな!」と検知し、強烈に `ReferenceError` を投げ捨てるのです。
4. TDZの解除
コードの実行が `let secretKey = ‘秘密のパスワード’` の行を通過した瞬間、初期化が完了し、TDZが解除されます。これ以降は、普通に変数を使えるようになります。

つまり、TDZとは「変数のメモリは確保されたけれど、まだコードの宣言文を通過していないため、アクセスが固く禁じられている時間的・空間的なゾーン」のことなのです。

—

なぜ、わざわざ面倒なTDZがあるのか?

「なんで `var` みたいに全部 `undefined` で初期化してくれないの? `let` って面倒くさいな……」と感じるかもしれません。しかし、これには言語設計上の極めて重要な理由があります。

もし `let` や `const` でも `var` と同じように勝手に `undefined` で初期化されてしまったらどうなるでしょう?

let isAuthorized = false;

// うっかり宣言前に使ってしまったとする
if (isAuthorized) {
console.log(‘ログイン成功!’);
}

// ずっと下の方で定義しているつもりだった
let isAuthorized = true; // 実際にはここで宣言

もし自動初期化されていたら、`isAuthorized` は意図せず `undefined`(真偽値の文脈では `false` 扱い)になり、エラーも起きずにバグが静かに潜り込みます。原因の分からない不具合に何時間も悩まされることになるでしょう。

JavaScriptの生みの親や仕様策定者たちは、こうした「バグの温床になりやすい曖昧な挙動」を排除したかったのです。「変数は、コード上で実際に宣言される場所より前では絶対に触らせない。ミスがあれば即座にエラーを出して気づかせる」。この堅牢性を担保するために生まれた仕組みが、TDZと `let`/`const` なのです。

—

現場で役立つ!今日からのコーディング鉄則

ここまでの話を整理して、明日からの開発にすぐ使えるベストプラクティスをまとめます。

1. `var` はもう使わない
`var` は巻き上げと自動初期化の挙動が複雑怪奇であり、スコープも関数単位と広すぎるため、現代のJavaScript開発では原則として使用禁止(アンチパターン)です。
2. 基本は `const`、再代入が必要な時だけ `let`
変数はできる限り `const` で宣言し、値が変わることが確実な場合のみ `let` を使いましょう。これにより、予期せぬ値の書き換えを防げます。
3. 宣言は、常にスコープの「最上部」で行う癖をつける
言語仕様上は巻き上げがあるとはいえ、コードを読む人間(チームメンバーや未来の自分)の脳内負荷を減らすため、変数や関数は使う前にしっかりと宣言する美しい書き方を心がけましょう。

—

いかがでしたでしょうか?
「コンパイルフェーズでのメモリ確保」「`var` の `undefined` 初期化」「`let`/`const` の初期化スキップによるTDZの発生」。この内部挙動のストーリーが頭に入っていれば、もう変数の巻き上げやエラーに怯える必要はありません。

ここをクリアしたあなたなら、JavaScriptのランタイムの挙動はもうバッチリマスターできていますよ!
自信を持って、クリーンで堅牢なコードを書き進めていきましょう。ハッピー・コーディング!

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