【実務・中級編】【初心者向け】巻き上げ(Hoisting)の正体:コードが実行される前に何が起きているのか? – JavaScriptコア文法・モダン言語仕様と変数・関数解析バイブル

コードレビューをしていて、未だに最も多く遭遇するバグの一つが「巻き上げ(Hoisting)」に起因する予期せぬ挙動だ。

「なぜか `undefined` が返ってくる」「`Cannot access ‘user’ before initialization` というエラーでアプリが落ちる」。
これらはJavaScriptがコードを上から順にただ実行しているという素朴なメンタルモデルを持っているがゆえに陥る罠である。V8エンジンをはじめとするモダンJSランタイムの内部挙動――すなわち、コードが実行される前に行われる「コンパイルフェーズ(作成フェーズ)」のメカニズムを理解していない証拠だ。

今回は、V8のヒープと実行コンテキストの裏側を覗きながら、巻き上げの正体を完全に暴き、明日からのコードレビューで一目置かれる堅牢な設計パターンを伝授する。

—

1. 実行の裏側:V8はコードを読む前に「何」をしているのか?

JavaScriptエンジン(V8など)は、私たちが書いたスクリプトをいきなり1行目から実行するわけではない。コードが実行される前には必ず「コンパイルフェーズ(Creation Phase)」が存在する。

このフェーズで何が行われているかと言うと、エンジンはスクリプト全体をスキャンし、変数や関数の宣言を見つけては「実行コンテキスト(Execution Context)」のメモリ空間(レキシカル環境)に事前に登録していく。これが、いわゆる「巻き上げ(Hoisting)」の正体だ。

物理的にコードの先頭に巻き上げられているわけではない。「実行前にメモリ上の領域が確保されている」という表現が、ランタイムの挙動としては100%正確である。

しかし、この「確保のされ方」が `var` と `let`/`const`、そして関数宣言では全く異なるため、開発者を混乱させるバグを生む。

—

2. `var` と `let`/`const` のメモリ割り当ての決定的な違い

宣言のキーワードによって、コンパイルフェーズでの扱いは明確に分かれる。

  • `var` の場合:

メモリ上に変数の場所が確保されると同時に、初期値として `undefined` が書き込まれる。そのため、宣言前にアクセスしてもエラーにならず単に `undefined` が返るという、極めてバグの温床になりやすい挙動を示す。

  • `let` / `const` の場合:

メモリ上の領域は確保されるものの、初期化は行われない。この宣言から実際の初期化コード(コード上の代入式)に到達するまでの空間は、「一時的死空間(Temporal Dead Zone: TDZ)」と呼ばれ、この間にアクセスするとJavaScriptエンジンは容赦なく `ReferenceError` を投げる。

ここで、プロダクションコードで絶対にやってはいけないアンチパターンと、それをどうリファクタリングすべきかを確認しよう。

アンチパターン:巻き上げに依存したカオスなコード

// 【危険なアンチパターン】
// varの巻き上げと関数宣言の巻き上げが混ざり合い、可読性が完全に死んでいる例

function processUserData() {
console.log(currentUser); // 1. ここは何が出力されるか? -> undefined

if (isLoggedIn) {
var currentUser = fetchUserFromCache(); // varは関数スコープを持つためブロックを無視する
}

// さらに、古い関数宣言は巻き上げられて上部にグローバル/関数スコープで定義される
function fetchUserFromCache() {
return { id: 42, name: ‘Architect’ };
}
}

このコードの何が最悪かと言うと、`var` は関数スコープであるため `if` ブロックの外(かつ宣言より上)でも `undefined` として参照できてしまう点だ。また、関数宣言の巻き上げにより、スコープのどこにその関数を書いても呼び出せてしまうため、コードの依存関係が追えなくなる。

—

3. 堅牢な設計パターン:TDZを味方につけ、意図しないバグをコンパイル時(または実行時即座)に検知する

テクニカルリードとしてチームに徹底してほしい原則はシンプルだ。

1. `var` は二度と使うな。すべて `const`、再代入が必要な場合のみ `let` を使え。
2. 変数は「使用する直前」ではなく「スコープの最上部」で宣言・初期化せよ(ただし `let`/`const` を使い、TDZを意識する)。
3. 関数は「関数式(アロー関数含む)」で変数に代入し、巻き上げによる曖昧さを排除せよ。

これらを遵守した、保守性の高いモダンなプロダクションコード例を見てほしい。

プロダクション品質のコード例:モジュール設計と非同期データフェッチ

/

  • @file user-controller.js
  • @description 堅牢なスコープ管理とTDZを活用したユーザーデータ処理モジュール

/

‘use strict’;

// 依存関係や定数は最上部で宣言(TDZを意図的に抜けさせた安全な状態を維持)
const API_RETRY_LIMIT = 3;
const DEFAULT_TIMEOUT = 5000;

/