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

こんにちは!フロントエンドからNode.jsの深層まで、日夜JavaScriptと向き合っているシニアアーキテクトです。

今回は、JavaScriptを学び始めた誰もが一度は混乱する「変数の宣言(var / let / const)とスコープ・巻き上げ(Hoisting)のメカニズム」について、一歩踏み込んで解説します。

特に「関数宣言と変数宣言が同じ名前のとき、どっちが優先されるの?」という疑問は、JavaScriptの心臓部であるエンジンの内部挙動(AST:抽象構文木とスコープの生成フェーズ)を理解することで、一気にクリアになります。

ここをしっかりマスターすれば、もう奇妙なバグに悩まされることはありません。さあ、一緒にエンジンの頭の中を覗いてみましょう!

—

1. そもそも「巻き上げ(Hoisting)」って何?

JavaScriptのコードは、私たちが書いた瞬間そのままブラウザやNode.jsで実行されるわけではありません。実行される前に、V8などのJavaScriptエンジンが「コードを読み解くフェーズ(解析フェーズ)」と「実際に上から実行していくフェーズ(実行フェーズ)」の2つに分けて処理しています。

この「解析フェーズ」のときに、エンジンはスコープ内にある変数や関数の宣言をあらかじめ見つけ出し、メモリ上に「お部屋(変数・関数名)」を確保します。これが、あたかもコードの先頭に「巻き上げられた」ように見える現象の正体です。

予備知識:var、let、constの巻き上げの違い

  • `var`: 宣言が巻き上げられ、同時に `undefined` で初期化されます。だから宣言前に呼んでもエラーになりません(`undefined` が返るだけ)。
  • `let` / `const`: 宣言は巻き上げられますが、初期化はされません。「一時的死Zone(Temporal Dead Zone: TDZ)」という領域に置かれるため、宣言前にアクセスすると非情にエラー(ReferenceError)を吐きます。

—

2. 本題:関数宣言と変数宣言の「優先順位」

では、もし同じスコープ内に「同じ名前の関数宣言」と「変数宣言(var)」があった場合、エンジンはどちらを優先してメモリに登録するでしょうか?

実際のコードで見てみましょう。

// さあ、このコードを実行すると、console.logには何が表示されるでしょうか?
console.log(myFunc);

var myFunc = “私は変数です”;

function myFunc() {
return “私は関数です”;
}

console.log(myFunc);

頭の中で実行結果を予測できましたか?
「変数に文字列を代入しているから、『私は変数です』かな?」それとも「関数が宣言されているから…?」

答えは、最初の `console.log(myFunc)` は「関数(の参照)」が出力され、最後の `console.log(myFunc)` は「私は変数です」が出力されます。

なぜこのような挙動になるのか、JavaScriptエンジンが内部で構築する「AST(抽象構文木)」とスコープの解析順序から紐解いていきましょう。

—

3. AST(抽象構文木)とエンジン解析の裏側

JavaScriptエンジンがコードをパース(構文解析)し、ASTを組み立ててスコープを生成する際、以下の厳格な優先順位ルールに従ってメモリ空間を構築します。

エンジンの脳内ルール(優先順位順)

1. 関数宣言(Function Declaration)が最優先で登録され、関数オブジェクトそのものがメモリに格納されます。
2. 変数宣言(var)がそれに続きますが、すでに同名の関数が登録されている場合、変数宣言の「上書き」は無視されます。(ただし、後からの「値の代入(`=`)」は実行フェーズで上書きされます)

つまり、先ほどのコードが解析されるとき、エンジンの中では次のような順番で処理が行われています。

// — 【ステップ1:解析フェーズ(巻き上げとスコープ構築)】 —

// 1. まず関数宣言が完璧な形で巻き上げられる
function myFunc() {
return “私は関数です”;
}

// 2. 変数宣言(var myFunc)も行われるが、
// すでに同名の関数が存在するため、エンジンは「お、もう同じ名前があるな。宣言はスルーしよう」と判断する
// (※ let / const の場合は、同名宣言ですでにエラーになります)

// — 【ステップ2:実行フェーズ】 —

// ① 1回目の出力:メモリには関数が残っているため、「[Function: myFunc]」が出力される
console.log(myFunc);

// ② 実行フェーズでの「代入」:ここで初めて、変数の値が「私は変数です」に書き換わる
myFunc = “私は変数です”;

// ③ 2回目の出力:変数に値が代入された後なので、「私は変数です」が出力される
console.log(myFunc);

このように、「関数宣言は変数宣言よりも圧倒的に強い優先度を持つ」というルールを知っているだけで、コードの挙動が完全に見通せるようになります。

—

4. 陥りやすい罠:現代のJavaScript(let / const)での挙動

ここまで `var` を中心に解説してきましたが、モダンな開発では `var` を使うことはほとんどなく、`let` や `const` を使いますよね。

では、`let` や `const` と関数宣言がバッティングした場合はどうなるでしょうか?

console.log(myFunc);

let myFunc = “私は変数です”;

function myFunc() {
return “私は関数です”;
}

このコードを現代の環境(Node.jsやモダンブラウザ)で実行すると、パース時(あるいは実行直前)に「SyntaxError: Identifier ‘myFunc’ has already been declared(すでに宣言されています)」というエラーが発生します。

`let` や `const` は、同じスコープ内での重複宣言を厳格に禁止しているため、古い `var` のような「こっそり上書き/無視される挙動」ではなく、バグを未然に防ぐためにエラーで止めてくれるのです。言語仕様の進化の歴史を感じる部分ですね。

—

5. まとめ:ここをクリアすれば基本はバッチリ!

  • 巻き上げは「解析フェーズ」に起きる: コードが実行される前に、エンジンが変数や関数の居場所を確保する仕組み。
  • 関数宣言のプライオリティは高い: 同名の `var` 変数宣言よりも関数宣言が優先され、メモリ上に保持される。
  • 値の代入は実行フェーズ: 巻き上げられるのは「宣言」だけであり、「代入(`=`)」はコードが上から流れてきたタイミングで行われる。
  • モダンな開発では重複を避ける: `let` / `const` を使えば重複宣言はエラーになるため、そもそも名前の衝突を気にしなくてよい設計にすることがベストプラクティス。

変数と関数の巻き上げ、そしてASTの解釈順序のイメージは掴めましたでしょうか?
この仕組みが頭の片隅に入っているだけで、複雑な非同期処理やスコープのバグに直面したときも、冷静にデバッグを行えるようになります。

ここをクリアすれば、JavaScriptの基本はバッチリマスターできたも同然です!
明日からのコーディングを、さらに自信を持って楽しんでいきましょう。