【実務・中級編】関数宣言と変数宣言の巻き上げ優先順位:AST生成フェーズにおける競合解決のルール – JavaScriptコア文法・モダン言語仕様と変数・関数解析バイブル

コードレビューの現場から:その「巻き上げ(Hoisting)」の挙動、本当に理解していますか?

テックリードの私だ。今日のコードレビューで、ジュニアエンジニアからこんな質問を受けた。

> 「同じスコープ内で同じ名前の変数と関数を宣言したら、一体どちらが優先されて実行されるんですか? なんとなく動いているけれど、実は正確なルールを分かっていなくて……」

この質問に即答できなかったり、「確か、後から書いた方かな?」などと曖昧な推測でやり過ごしたりしているなら、危険信号だ。その認識の甘さが、プロダクション環境での難解なバグ――とりわけ、トランスパイラやバンドラを通した際に突如として現れる「TDZ(Temporal Dead Zone)関連のエラー」や「意図しないundefinedの混入」を引き起こす。

今回は、JavaScriptエンジン(V8など)がソースコードをどのように読み解き、抽象構文木(AST)を生成し、実行コンテキストを構築するのか。その決定論的な競合解決のルールを、ランタイムの深部から徹底的に紐解いていこう。

—

1. 表面的な「巻き上げ」の誤解を正す

世の多くの解説記事には、「JavaScriptの変数はコードの最上部に巻き上げられる」と書かれている。しかし、チーフアーキテクトとして言わせてもらえば、この説明は半分正しくて半分不親切だ。

コードが実際に物理的に上へ移動しているわけではない。
JavaScriptエンジンはコードを実行する前に、必ず「生成フェーズ(Creation Phase)」と「実行フェーズ(Execution Phase)」の2つのステップを踏む。

1. 生成フェーズ: エンジンがコードをスキャンし、AST(抽象構文木)を構築しながら、変数や関数の識別子をメモリ(レキシカル環境)に登録していく。
2. 実行フェーズ: 実際にコードが上から順に評価・実行される。

この生成フェーズにおける登録の順序と優先順位こそが、今回のテーマの核心である。

—

2. AST生成フェーズにおける競合解決のルール

同一スコープ内で、`var` による変数宣言と、`function` による関数宣言が同じ名前(識別子)を持つとき、V8をはじめとするECMAScript準拠のエンジンは、厳格なアルゴリズムに従ってこれを処理する。

結論から言おう。関数宣言は、`var` 変数宣言よりも常に優先してメモリに登録される。

正確な優先順位のルールは以下の通りだ。

1. 関数宣言(Function Declaration): 最優先で登録され、関数オブジェクトそのものがメモリ上のスロットにバインドされる。
2. 変数宣言(`var`): 関数宣言の後に処理される。ただし、すでに同じ名前の識別子(関数)がスコープ内に存在する場合、`var` の宣言(およびその `undefined` による初期化)は無視される。
3. 変数宣言(`let` / `const`): これらは「巻き上げ」は行われるものの、初期化が行われないTDZ(一時的死領域)に置かれるため、同名の関数宣言や `var` と競合した時点で構文エラー(SyntaxError)を吐く。

脳内トレース:実際のコードで挙動を確認する

以下のコードを見てほしい。あなたはこの実行結果を正確に予測できるだろうか?

/

  • 競合解決のメカニズムを検証するサンプルコード

/
function evaluateScopeConflict() {
// Q. この console.log は何を出力するでしょうか?
console.log(typeof targetIdentifier); // ①

var targetIdentifier = ‘これは変数です’; // ②

function targetIdentifier() { // ③
return ‘これは関数です’;
}

console.log(typeof targetIdentifier); // ④
}

evaluateScopeConflict();

実行結果の予測とメカニズム

  • ①の出力: `”function”`
  • ④の出力: `”string”`

なぜ①の時点で `undefined` でも文字列でもなく `”function”` になるのか。
それは、生成フェーズにおいてエンジンが以下のようにメモリを構築したからだ。

1. スコープの走査中、`function targetIdentifier()` を発見し、識別子 `targetIdentifier` に関数オブジェクトを割り当てる。
2. 次に `var targetIdentifier = ‘これは変数です’` を発見するが、すでに同名の識別子が関数として登録されているため、 `var` による上書き(初期化)は完全にスキップされる。
3. 実行フェーズに入り、①の時点ではまだ `var` の代入文(`targetIdentifier = ‘これは変数です’`)に到達していないため、メモリに残っているのは最初期に登録された「関数」のままとなる。
4. ④の行を通過した瞬間に、代入文によって `targetIdentifier` の値が文字列 `’これは変数です’` に書き換わるため、④では `”string”` が出力される。

これが、AST解析における競合解決のリアルな姿だ。

—

3. なぜこの挙動を知る必要があるのか?(実務上のリスク)

「へえ、面白い言語仕様だね」で済ませては、プロのフロントエンドエンジニアとは言えない。この挙動を知らないと、以下の重大なバグを生む。

リスク1:意図しない関数(あるいは変数)のオーバーライド

大規模なコンポーネントやレガシーなユーティリティモジュールにおいて、偶然同じ変数名と関数名が混在した場合、上記のような優先順位の仕様によって、デバッグが極めて困難な値のすり替わりが発生する。

リスク2:TypeScriptや最新のバンドラへの過信

「自分はTypeScriptを書いているから大丈夫だ」と思った読者、甘い。TypeScriptの型チェックはコンパイル時に消え去る。最終的に出力されるのは素のJavaScript(ES5やターゲットに応じたESNext)だ。特に `var` を直接書かなくとも、トランスパイラの変換過程や、スコープの狭いクロージャ内での巻き上げ挙動に起因するバグは、プロダクションの闇として頻出する。

—

4. チーフアーキテクトが推奨する「バグをゼロにする設計パターン」

この仕様の挙動をハックしてトリッキーなコードを書く必要など微塵もない。プロフェッショナルな開発チームにおけるコードレビューでは、以下の方針を絶対遵守とせよ。

1. `var` の使用を完全に禁止し、`let` / `const` を強制する

現代のJavaScript開発において、`var` を使う理由は1ミリも存在しない。ES2015(ES6)以降、ブロックレベルスコープを持つ `let` と `const` が標準化された。
`let` や `const` を同名で再宣言しようものなら、エンジンは実行するまでもなくSyntaxErrorを投げてくれる。ランタイムエラーや予期せぬ値のすり替わりを、コンパイル(ビルド)の段階で完全に検知できる恩恵を捨ててはならない。

2. 関数は「関数式(Function Expression)」で定義する癖をつける

関数宣言(`function foo() {}`)は巻き上げの恩恵を強く受けるため、スコープ内のどこからでも呼び出せるというメリットがある反面、今回のような意図しない競合の原因にもなる。
コンポーネントのメソッドやローカルなヘルパー関数を定義する際は、次のように `const` とアロー関数、あるいは関数式を組み合わせることで、巻き上げによる曖昧さを完全に排除できる。

/

  • 保守性の高いモジュール設計の模範例
  • – var は一切使わない
  • – 関数は const とアロー関数で定義し、必ず定義した行より後から呼び出す(TDZの強制)

/
const UserAuthManager = (() => {
// 状態をカプセル化
let activeSession = null;

// ヘルパー関数も const で定義し、巻き上げによる曖昧さを排除
const validateToken = (token) => {
return typeof token === ‘string’ && token.length > 10;
};

return {
login(token) {
if (!validateToken(token)) {
throw new Error(‘Invalid token structure.’);
}
activeSession = { token, loggedInAt: Date.now() };
return activeSession;
},
logout() {
activeSession = null;
}
};
})();

// 実行
// UserAuthManager.login(‘valid_token_string_123’);

—

5. パフォーマンスとV8エンジンのメモリ空間への影響

最後に、V8エンジンの内部構造に少し踏み込んでおこう。

`var` や重複した宣言が多用されたスコープは、V8のコンパイラ(Ignition / TurboFan)にとって最適化が困難なコード(Optimization Killerとは言わないまでも、Hidden Classやスコープ内の変数ルックアップのコストを増大させる要因)になり得る。

レキシカル環境(Lexical Environment)において、識別子の解決が「関数が優先され、変数が無視される」ような複雑な解決ロジックをたどるコードは、人間にとってもエンジンにとっても認知負荷が高い。

  • コードは上から下へ、直線的に読めるように書く。
  • 同じスコープ内で同じ名前を使い回さない(シャドーイングの適切な管理)。
  • モダンな構文(`const` / `let`)を徹底し、曖昧な巻き上げの挙動に依存しないコードベースを構築する。

これらを徹底することが、結果としてV8の最適化エンジンをスムーズに働かせ、高速でメモリ効率の良いフロントエンドアプリケーションを実現する最短の道なのだ。

今日のレビューから、君の書くコードの質が変わることを期待している。

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