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

こんにちは!JavaScriptの奥深い世界へようこそ。
日々コードを書いていると、「あれ、なんでこの変数、値が入る前にundefinedって言われるんだろう?」とか、「関数宣言と変数宣言って、どっちが先に読み込まれるんだっけ?」と疑問に思ったことはありませんか?

他のプログラミング言語、例えばJavaやC++からやってきた方にとっては、JavaScriptのこの挙動は最初はちょっと魔法のように、あるいはバグのように感じられるかもしれません。

でも、安心してください。これはJavaScriptのエンジン(V8など)が、コードを実行する前に裏側で行っている「ある特別なルール」に基づいた正確な処理なんです。ここをクリアすれば、JavaScriptの変数や関数の動きが手に取るようにわかるようになりますよ。

今回は、JavaScriptの頭脳である「パーサー」がコードをどう読み解いているのか、AST(抽象構文木)の視点も交えながら、優しく、そして本質的に紐解いていきましょう!

—

1. そもそも「巻き上げ(Hoisting)」ってなんだっけ?

JavaScriptのコードは、私たちが上から下へ一行ずつ実行している…ように見えますが、実は実行される前段階(コンテキストの生成フェーズ)で、コード全体を一度スキャンしています。

このスキャン時に、変数や関数の宣言をスコープの先頭に「持ち上げた」かのように内部で記憶する現象を、巻き上げ(Hoisting)と呼びます。

例えば、こんなコードを見たことはありませんか?

// 関数を宣言する前に呼び出している!
sayHello(); // ちゃんと動く!

function sayHello() {
console.log(“こんにちは!”);
}

「えっ、定義する前に使ってるのにエラーにならないの?」と驚きますよね。
これは、V8などのエンジンが実行前に `function sayHello()` の存在を先に見つけ出して、メモリ上に登録しているからなんです。

—

2. 変数宣言(var)と関数宣言のガチンコ勝負! 優先順位の謎

では、ここからが本題です。
もし、「同じ名前の関数宣言」と「変数宣言(var)」がバッティング(衝突)したら、どちらが勝つのでしょうか?

次のコードを見て、どんな出力になるか予想してみてください。

// さあ、何が出力されるでしょうか?
console.log(typeof mischief);

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

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

console.log(typeof mischief);

……正解は、以下のようになります。

[実行結果]
“function”
“string”

最初の `console.log` では、なんと `”function”` と出力されます。「変数に文字列を代入したはずなのに、なぜ関数になっているんだ?」と思いますよね。

これが、JavaScriptのパーサーが持つ「巻き上げの優先順位のルール」の正体です。

パーサーの頭の中:AST(抽象構文木)はどう解析しているか?

JavaScriptのエンジンは、コードを読み込むときに「AST(Abstract Syntax Tree:抽象構文木)」というツリー構造のデータに変換します。この解析フェーズにおいて、メモリ(変数環境)への登録順序には厳密なルールがあります。

優先順位の勝敗は、ズバリこうなっています。

1. 第1位(最強): `function` による関数宣言
2. 第2位: `var` による変数宣言
3. 第3位: `let` / `const` による変数宣言(※これらは巻き上げられますが、初期化されないためエラーになります)

エンジンがコードをスキャンするとき、まず最初に「関数宣言」をごっそり見つけてメモリにガッチリ固定します。その後に「変数宣言(var)」を見つけますが、すでに同名の識別子が関数で登録されている場合、変数の宣言は無視(上書きなし)されます。

そのため、先ほどのコードの裏側では、エンジンによって次のように解釈されています。

// — JavaScriptエンジン内部の解釈(イメージ) —

// 1. 最優先で関数宣言が登録される
function mischief() {
return “私は関数です”;
}

// 2. varの宣言は、同名の識別子がすでに存在するため無視される
// var mischief; (実質的に何もしない)

// — ここから実際のコードの実行フェーズ —

// この時点では mischief は「関数」としてメモリにいる
console.log(typeof mischief); // “function”

// ここで初めて変数としての「代入」が実行され、中身が文字列に書き換わる
mischief = “私は変数です”;

// だからここは “string” になる
console.log(typeof mischief); // “string”

すごくスッキリ納得できますよね! 「関数宣言は変数宣言よりも常に格上で、先住権を持っている」と覚えると間違いありません。

—

3. 現代のモダンJS(let / const)ではどうなるの?

「でもさ、実務ではもう `var` なんて使わないで、`let` や `const` を使うよね?」
その通り!よくぞ気づいてくれました。優秀なエンジニアの証拠です。

では、`let` や `const` で同じことをやったらどうなるでしょうか?

console.log(typeof mischief);

let mischief = “私はletの変数です”;

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

これを実行すると……、慈悲のないエラーが飛んできます。

[実行結果]
Uncaught SyntaxError: Identifier ‘mischief’ has already been declared
(または、一時的デッドゾーン(TDZ)に起因するエラー)

モダンなJavaScript(ES6以降)では、同じスコープ内で同じ変数名を `let` で二重に宣言することは、文法エラー(SyntaxError)として厳しく弾かれるようになっています。

JavaScriptの生みの親であるBrendan Eich氏も、「あの頃の `var` の仕様は自由度が高すぎた(=バグの温床になりやすかった)」と認めており、現代の私たちには、予測可能性の高い安全な `let` や `const` が用意されています。

—

4. まとめ:今日から使えるベストプラクティス

ここまでの知見をギュッと凝縮しましょう。

  • 巻き上げの仕組み: JavaScriptは実行前にコードをスキャンし、宣言を先頭に引き上げる。
  • 優先順位: 「関数宣言」は「変数宣言(var)」よりも優先される。同名がある場合、変数の `var` 宣言は関数に負ける。
  • 実践での心得:
  • そもそも、同じスコープ内で変数と関数に同じ名前をつけるような紛らわしいコードは書かない!
  • 変数や関数は、「使う前に必ず宣言・定義する」というクリーンな書き振りを徹底する。
  • `var` は使わず、スコープが厳密に管理される `let` と `const` を使い倒す。

このASTの解析順序やエンジンの裏側の動き(メモリへの登録フェーズと実行フェーズの分離)を頭の片隅に置いておくだけで、複雑なコードリーディングや思わぬバグに遭遇したときも、「あ、これはあそこの巻き上げが原因だな」と秒速で原因を特定できるようになります。

ここをクリアしたあなたなら、JavaScriptの変数スコープや実行コンテキストの基本はもうバッチリマスターできていますよ!自信を持って、次のステップへ進んでいきましょう。

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