こんにちは!フロントエンドからNode.jsの深層まで、日々のコードに魂を込めているエンジニアのあなたへ。
今回は、JavaScriptの挙動において誰もが一度はハマる、そして中級者への扉を開くための最重要テーマ「変数と関数宣言の巻き上げ(Hoisting)と、AST(抽象構文木)生成フェーズにおける優先順位の決定プロセス」についてお話ししていきますね。
「JavaScriptのコードを書いたら、なぜか`undefined`が出たり、関数が突然使えたりして混乱した……」そんな経験はありませんか?
ここをクリアすれば、JavaScriptのランタイムが頭の中で完全に可視化できるようになり、バグを未然に防ぐ確かな力が身につきますよ。一緒に基本から本質までを紐解いていきましょう!
—
1. そもそも「巻き上げ(Hoisting)」って何?
JavaScriptのコードは、私たちが書いた瞬間そのまま上から下に一筋縄で実行されているわけではありません。
JavaScriptエンジン(V8など)は、コードを実行する前に必ず「Creation Phase(生成フェーズ)」という下準備の時間を設けています。
このフェーズで、エンジンはソースコードをスキャンし、変数や関数の宣言をコードの先頭に「巻き上げた(持ち上げた)」かのように扱って、メモリ上の領域(Environment Record / 環境レコード)に登録します。これが、いわゆる「巻き上げ」の正体です。
ですが、ここで重要なのは「変数」と「関数宣言」では、その登録のされ方と優先順位が全く異なるという点です。ここを勘違いしていると、予期せぬバグに足元をすくわれてしまいます。
—
2. 【基本】変数と関数宣言の挙動の違いを見てみよう
まずは、実際のコードでそれぞれの違いを確認してみましょう。
// まだコードの下の方で宣言している変数や関数を、上から呼び出してみます
console.log(myVariable); // 出力はどうなるでしょう?
sayHello(); // これはエラーになる?それとも動く?
var myVariable = “こんにちは、JavaScript!”;
function sayHello() {
console.log(“関数が呼ばれました!”);
}
このコードを実行すると、次のような結果になります。
undefined
関数が呼ばれました!
不思議ですよね? `myVariable` は `undefined` となり(エラーで止まらない)、`sayHello` に至っては、定義より上で呼び出しているにもかかわらず完璧に実行されています。
なぜこのような現象が起きるのか?それを理解するために、V8エンジンの頭の中(ASTとEnvironment Record)を覗いてみましょう。
—
3. AST生成フェーズにおける「優先順位」の決定プロセス
JavaScriptエンジンがソースコードを読み込むとき、パーサーはコードを解析してAST(抽象構文木)というツリー構造を作り上げます。この解析のプロセスにおいて、Environment Recordへの登録には厳密な優先順位とルールが存在します。
脳内イメージしやすいように、生成フェーズでのエンジンの動きを順番に追ってみましょう。
階層化された登録のルール
1. 関数宣言(Function Declaration)の最優先登録
エンジンがコード全体を走査した際、`function 識別子() {}` という構文を見つけると、その関数オブジェクト全体をメモリ上に作成し、環境レコードに最優先で登録します。だからこそ、定義より上で呼び出しても何の問題もなく実行できるのです。
2. 変数(var)の控えめな登録
一方、`var` による変数宣言は、識別子(変数名)だけが環境レコードに登録されますが、その初期値は一律で `undefined` が割り当てられます。実際に代入コード(`= “…”`)が実行されるのは、コードが上から下に流れる「実行フェーズ」になってからです。そのため、宣言より上で参照すると `undefined` が返されるわけです。
3. let / const(Temporal Dead Zone)
ちなみに、モダンな `let` や `const` も巻き上げ自体は行われますが、初期値が割り当てられません。そのため、宣言の前にアクセスすると「Temporal Dead Zone(一時的死空間)」という厳格なガードに阻まれ、盛大に `ReferenceError` が発生します。ここは初心者キラーなので要注意ですよ!
—
4. 衝突が起きたとき?ASTが決定する優先順位のリアル
もし、同じスコープ内に「同じ名前」の関数宣言と変数宣言が存在したら、エンジンはどちらを優先するのでしょうか?
実際のコードで実験してみましょう。
// 同じ名前の変数と関数を用意します
var getName;
function getName() {
return “関数としての名前”;
}
console.log(typeof getName); // 実行結果は何になる?
console.log(getName); // 中身はどうなっている?
上記のコードを実行すると、出力はこうなります。
function
[Function: getName]
なんと、変数として `var getName;` と書いて後から上書きしているように見えても、結果は「関数」のままになります。
なぜこの結果になるのか?(エンジンの内部視点)
生成フェーズにおいて、ASTを構築するパーサーは次のような優先順位でEnvironment Recordを構築します。
1. まず、関数宣言が優先的にスキャンされ、メモリ空間にその名前と関数本体がガッチリ陣取ります。
2. その後、変数宣言(var)がスキャンされますが、「すでに同名の識別子が環境レコードに存在する場合、変数の宣言は無視(上書きなし)される」というルールが働きます。
ただし、注意してください。これはあくまで「宣言」のフェーズの話です。もし実行フェーズで明示的な「代入」が行われた場合は話が変わります。
var getName = “ただの文字列だよ”; // 実行フェーズでの代入
function getName() {
return “関数としての名前”;
}
console.log(typeof getName); // ここはどうなる?
実行フェーズに入ると、コードは上から順に実行されます。そのため、`getName = “ただの文字列だよ”` が実行された瞬間に、メモリ上の `getName` は関数から文字列へと上書きされ、出力は `string` に変わります。
この「生成フェーズでの宣言の優先順位」と「実行フェーズでの値の代入」を頭の中で綺麗に分離できるようになると、コードの挙動で迷うことが本当になくなりますよ。
—
5. まとめ:今日から実践できるクリーンなコードの書き方
今回は、AST生成フェーズにおける関数と変数の巻き上げの優先順位について、エンジンの内部挙動を交えて解説しました。
- 関数宣言は最優先でメモリにロードされ、本体ごと使える状態で巻き上げられる。
- var変数は識別子のみが登録され、値は `undefined` で初期化される。
- 同名のエントリがある場合、生成フェーズでは関数宣言が優先される。
こうしたJavaScriptの仕様(言語仕様上の仕様)を知ることは大切ですが、実際の開発現場では、「巻き上げに依存したコードを書かないこと」がプロフェッショナルへの第一歩です。
- 変数や関数は、使用する前に必ず宣言・定義する(できれば `var` ではなく `let` や `const` を使う)。
- スコープの先頭で宣言をまとめる(いわゆる変数宣言のホイスティングを意識させないクリーンな構造にする)。
ここをクリアすれば、あなたの書くJavaScriptコードの品質は一段と洗練され、バグの温床を綺麗に排除できるようになります。
基礎の仕組みを味方につけて、最高にエレガントなコードを一緒に書いていきましょう!