こんにちは!日々の開発、本当にお疲れ様です。
JavaScriptを書いていて、「あれ、この関数、定義する前に呼び出してるのに動くぞ…?」「でもこっちは `TypeError` になる…なんでだ?」と首を傾げた経験はありませんか?
ネットの記事を検索すると「巻き上げ(Hoisting)とは、変数の宣言が上部に持ち上げられる現象です」となんとなく説明されていますよね。でも、実際のV8エンジンやJavaScriptパーサは、コードを文字通り「物理的に持ち上げている」わけではありません。
今回は、JavaScriptの頭脳であるAST(抽象構文木)の生成プロセスとスコープの構築順序を覗き見しながら、関数宣言と関数式の巻き上げの優先順位を完全攻略していきましょう。ここをクリアすれば、もうJavaScriptのスコープ周りで迷うことはなくなりますよ。バッチリマスターしていきましょう!
—
1. そもそも「巻き上げ」の正体とは?
多くの入門書では「コードが実行される前に、JSエンジンが宣言を一番上に移動させる」と説明されますが、これはあくまで初心者に向けたイメージ図に過ぎません。
実際のランタイム(V8など)の挙動はこうです:
1. 構文解析(Parsing)フェーズ: JSエンジンはコードを実行する前に、テキストを読み込んでAST(抽象構文木)という木構造のデータに変換します。この段階で、どこに変数や関数があるのかをスキャンし、メモリ(環境レコード / Lexical Environment)に登録します。
2. 実行(Execution)フェーズ: 実際にコードが上から順に実行されます。
つまり、巻き上げとは「コードが物理的に移動する魔法」ではなく、「実行前のスキャン段階で、メモリへの登録が完了している」というエンジンの裏側の仕組みなんです。
—
2. 関数宣言 vs 関数式:決定的な違いを生むASTの構造
では、本題である「関数宣言」と「関数式」の巻き上げの挙動と、その優先順位の違いを見ていきましょう。
まずは、よくあるこの2つの書き方を思い出してください。
// A. 関数宣言 (Function Declaration)
function sayHello() {
console.log(“こんにちは!”);
}
// B. 関数式 (Function Expression)
const sayHi = function() {
console.log(“やあ!”);
};
一見すると似たような関数定義ですが、「いつメモリに中身(関数オブジェクト)が結びつけられるか」が全く異なります。
関数宣言の挙動(完全な巻き上げ)
関数宣言は、パーサがコード全体をスキャンした瞬間、識別子の名前とその関数オブジェクトの本体がセットでメモリに登録されます。そのため、定義の「前」で呼び出しても何の問題もなく実行できます。
関数式の挙動(変数の巻き上げのみ)
一方、関数式は `const` や `let`(または `var`)といった変数宣言の構文の一部です。
`const sayHi = function() {}` というコードに直面したとき、ASTパーサは以下のように解釈します。
- `const` で宣言された変数 `sayHi` は、スコープの先頭で「メモリの箱」だけが確保されます(これが `let` や `const` の一時的死区間:TDZです)。
- しかし、`=` の右側にある「関数の中身」は、実際にその行が実行されるまで評価されません。
したがって、定義の前に呼び出すと、「箱はあるけど中身が空(あるいは未初期化)」なのでエラーになるのです。
—
3. 脳内AST解析!「関数宣言」と「変数」が同名だったらどうなる?
ここで、中級者へのステップアップとして、意地悪なコードを考えてみましょう。
もし、同じスコープ内に「関数宣言」と「同名の変数(関数式や値)」が混在していた場合、V8エンジンはどちらを優先してメモリに登録するでしょうか?
以下のコードの出力結果を、脳内で予測してみてください。
// 脳内トレースクイズ
console.log(typeof score); // ① ここは何が出力される?
var score = 100; // 変数宣言
function score() { // 関数宣言
return 50;
}
console.log(typeof score); // ② ここは何が出力される?
正解の動きを、AST解析の目線で順番に追ってみましょう。
解析ステップの裏側
1. フェーズ1(スキャンと巻き上げ):
JSエンジンがコードをパースし、変数と関数を環境レコードに登録します。
- まず、`function score()`(関数宣言)が見つかり、`score` という名前で関数オブジェクトがメモリに登録されます。
- 次に、`var score`(変数宣言)が見つかりますが、すでに `score` という名前は登録されているため、重複する変数宣言は無視(上書きなし)されます。
2. フェーズ2(実行フェーズ):
- ①行目 (`console.log(typeof score)`): メモリには関数宣言によって関数が登録されているため、出力は `”function”` になります。「変数宣言の `var score = 100` があるなら、`undefined` になるのでは?」と思いがちですが、関数宣言の方が優先度が高く、すでに巻き上げられているため関数になるのです。
- その後、コードが上から実行され、`score = 100` の行に達した時点で、`score` 変数の値が関数から数値の `100` に上書きされます。
- ②行目 (`console.log(typeof score)`): 現在の `score` は `100` なので、出力は `”number”` になります。
非常にスリリングで、かつJavaScriptの仕様の深みを感じる挙動ですよね。実際の開発で同名の変数と関数を同じスコープに書くべきではありませんが、この優先順位のメカニズムを知っておくことで、予期せぬバグに直面したときも迷わず原因を特定できるようになります。
—
4. まとめ:今日から使える「安全なコードの書き方」
ここまでの知見をまとめます。
- 関数宣言 (`function foo() {}`):
名前も中身も完全に巻き上げられるため、定義の前に呼び出すことが可能。ただし、コードの可読性が下がる原因にもなるため、特別な理由がない限りは利用を控えめに。
- 関数式 (`const foo = function() {}` / アロー関数):
変数の巻き上げルール(TDZ)に依存するため、定義する前に呼び出すと `ReferenceError` や `TypeError` になる。
これが現代のモダンなJavaScriptにおけるベストプラクティスです。
「上から順に読みやすいコードを書く」という原則を徹底するためにも、基本的には関数式(特にアロー関数)を使い、必ずコードの定義後に呼び出すスタイルを心がけましょう。
ここをクリアできれば、あなたのJavaScriptのコードリーディング能力は確実に一段階上のステージに到達しています。ぜひ、明日のコーディングからこのASTの視点を意識してみてくださいね。バッチリマスターです!