【テクニカル・上級編】【中級者向け】関数宣言と関数式の巻き上げ優先順位:AST(抽象構文木)から読み解く解析順序 – JavaScriptコア文法・モダン言語仕様と変数・関数解析バイブル

関数宣言と関数式の巻き上げ優先順位:AST(抽象構文木)から読み解く解析順序

JavaScriptエンジニアであれば、「巻き上げ(Hoisting)」という言葉を一度は耳にしたことがあるだろう。しかし、`var`、関数宣言(Function Declaration)、そして関数式(Function Expression)が同一スコープ内に混在したとき、V8などのJSエンジンがパース段階でどのような優先順位で識別子をバインドしているかを正確に説明できる者は少ない。

本稿では、一般的な解説書にあるような「コードが上部に持ち上げられる」という幻想を完全に破壊し、V8のパーサーが生成するAST(抽象構文木)とスコープ(Scope)解析フェーズの物理的挙動から、真の巻き上げのメカニズムを解き明かす。

—

1. 巻き上げの本質:コードの物理的移動ではなく「スコープの事前割当」

まず大前提として、JavaScriptのソースコードが実行される前には、必ずパース(構文解析)とコンパイル(バイトコード生成)のフェーズが存在する。エンジンはコードを上から下へ実行する前に、スコープ内に存在する変数や関数の識別子をスキャンし、メモリ上の環境レコード(Environment Record)にスロットを割り当てる。

これが「巻き上げ」の正体である。コードが物理的に上に移動するわけではなく、実行コンテキストの生成フェーズにおいて、識別子のメモリ領域が事前に確保される現象に他ならない。

では、以下のコードスニペットを見てほしい。

console.log(typeof foo); // 1. 出力は何になるか?
var foo = 10;

function foo() {
return ‘function’;
}

console.log(typeof foo); // 2. 出力は何になるか?

多くの開発者は、`var foo` と `function foo()` が競合した際、どちらが優先されるかで混乱する。これを正確に予測するためには、V8のAST解析プロセスを脳内で再現する必要がある。

—

2. ASTとスコープ解析:関数宣言 vs 変数の優先順位

V8のパーサー(Ignition / TurboFanパイプラインの前段)は、ソースコードをトークン化し、ASTを構築する。この過程で、関数宣言はスコープの先頭において最も高い優先順位で識別子と関数オブジェクトのポインタで初期化される。

一方、`var` による変数宣言は、識別子のスロットを確保し `undefined` で埋めるが、すでに同名の関数宣言によって識別子がバインドされている場合、`var` の宣言はその初期化を上書きしない(無視される、あるいは巻き戻されない)。

上記のコードの挙動を、V8の内部挙動に沿ってステップバイステップで追ってみよう。

フェーズ1:Creation(生成フェーズ / スコープの構築)

1. エンジンはスコープ内をスキャンし、まず関数宣言 `function foo() {}` を検出する。識別子 `foo` に関数オブジェクトの参照がバインドされる。
2. 次に変数宣言 `var foo` を検出する。しかし、すでに同名の `foo` がスコープ内に存在するため、変数の巻き上げによる `undefined` での上書きはスキップされる。

フェーズ2:Execution(実行フェーズ)

1. `console.log(typeof foo);`

  • スコープ内の `foo` は関数オブジェクトを指しているため、出力は `’function’` となる。

2. `var foo = 10;`

  • 実行時に `foo` に数値の `10` が代入され、識別子の参照先が関数からプリミティブな数値に書き換わる。

3. `console.log(typeof foo);`

  • 現在の `foo` は `10` であるため、出力は `’number’` となる。

このように、関数宣言は変数の宣言(`var`)よりも常に優先される。これがAST解析における大原則である。

—

3. 関数式(Function Expression)が絡む場合の罠

では、これが「関数宣言」ではなく「関数式」である場合はどうなるか。以下のコードを検証する。

‘use strict’;

console.log(typeof bar); // 出力は?

var bar = function bar() {
return ‘expression’;
};

console.log(typeof bar);

ここでの主役は `var bar` と、代入される無名(あるいは名前付き)の関数式である。

1. 巻き上げの対象になるのは `var bar` のみである。関数式自体は、右辺の代入演算子(Assignment Expression)の一部であるため、巻き上げの対象外となる。
2. したがって、最初の `console.log(typeof bar);` の時点では、`bar` は `var` によって `undefined` として巻き上げられているため、出力は `’undefined’` になる。
3. 実行フェーズに達して初めて関数式が評価され、`bar` に関数が代入される。

ASTの構造から見る決定的な違い

| 構文の種類 | AST上のノード名 | 巻き上げ時の挙動 | 初期値 |
| :— | :— | :— | :— |
| 関数宣言 | `FunctionDeclaration` | 識別子+関数本体が完全に巻き上げられる | 関数オブジェクトへの参照 |
| 変数宣言 (`var`) | `VariableDeclaration` (`var`) | 識別子のみが巻き上げられる | `undefined` |
| 関数式 (`var`代入) | `VariableDeclaration` + `FunctionExpression` | 変数部分のみ巻き上げ、式は実行時評価 | `undefined` (実行前) |
| `let` / `const` | `VariableDeclaration` (`let`/`const`) | 巻き上げは起きるがTDZ(Temporal Dead Zone)に置かれる | 未初期化(アクセスでReferenceError) |

—

4. 実戦的コード解析:複雑なスコープとシャドーイングの極限

シニアエンジニアとして遭遇する最も厄介なケースは、関数引数、関数宣言、そして `let`/`const` が同一スコープ内で名前空間を競合(シャドーイング)させている場合だ。以下のコードの挙動を完全に脳内トレースできるだろうか?

const x = 1;

function evaluate(x) {
console.log(x); // ① 何が出力されるか?

var x = 2;

function x() {
return ‘inner’;
}

console.log(x); // ② 何が出力されるか?
}

evaluate(10);

厳密なランタイム解析

この関数 `evaluate(x)` のスコープ内では、以下の識別子 `x` が競合している。
1. 仮引数 `x` (呼び出し時に `10` が渡される)
2. 関数宣言 `function x() {}`
3. 変数宣言 `var x = 2;`

V8のパーサーとスコープ生成器は、関数エントリー時に以下のように環境レコードを構築する。

1. 引数のバインド: 仮引数 `x` に `10` がバインドされる。
2. 関数宣言の処理: 同スコープ内に `function x() {}` が存在するため、引数の `x` のバインドは、関数宣言のフェーズによって関数オブジェクトへの参照で上書きされる。
3. 変数 `var x` の処理: すでに `x` は存在するため無視される。

結果として、関数が実行された瞬間のローカル変数 `x` は、引数の `10` でも `undefined` でもなく、関数宣言された `x`(関数オブジェクト)になっている。

  • ①の `console.log(x)` の出力:`[Function: x]`
  • その後、コードの実行により `var x = 2;` が評価され、`x` に数値の `2` が代入される。
  • ②の `console.log(x)` の出力:`2`

これが、ASTとスコープ解析の順序を熟知していないとハマる、JavaScriptの最も深遠な挙動の一つである。

—

5. セキュリティ・文脈:この知識がなぜ脆弱性防御に直結するのか

「なぜ変数の巻き上げや関数の優先順位といった基本仕様をここまで深く知る必要があるのか?」と疑問に思うかもしれない。答えは明確だ。プロトタイプ汚染(Prototype Pollution)や、動的なコード評価(`eval`, `Function`コンストラクタ)を伴うメタプログラミング、あるいは複雑なASTトランスパイル(BabelやTypeScript compiler)を行うモジュールの挙動をハック・デバッグする際、このレイヤの理解が不可欠だからである。

特に、サプライチェーン攻撃において悪意あるパッケージがグローバルスコープやプロトタイプチェーンを汚染する際、変数のスコープ解決順序や巻き上げの隙を突いた変数ハイジャック(Variable Shadowing利用のロジックバイパス)が仕込まれるケースがある。

チーフアーキテクトとして断言する。JavaScriptを「なんとなく動く言語」から「物理的に制御可能なランタイム」へと昇華させるのは、こうしたASTとV8エンジンの内部挙動に対する絶対的な理解力に他ならない。コードの行間にあるランタイムの息吹を感じ取れ。それこそが、真のフルスタックエンジニアの領域である。

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