こんにちは!JavaScriptのコアな仕組み、特に裏側で何が起きているのか気になってこのページにたどり着いたのですね。素晴らしい探究心です!
他のプログラミング言語、例えばJavaやC#などを触ってきた方からすると、JavaScriptの変数や関数の挙動は「なんだか魔法のようで予測しづらい…」と感じる瞬間があるかもしれません。コードを書いた順番通りに上から下へ実行されるはずなのに、なぜか定義する前の関数が呼べてしまったり、変数が突然 `undefined` になったりする。
ここをクリアすれば、JavaScriptのランタイムが頭の中で手に取るように見えてきますよ。今日は、JavaScriptのソースコードがブラウザやNode.jsに読み込まれた瞬間、V8などのエンジンが最初に何をしているのか、AST(抽象構文木)の視点から一緒に深く潜ってみましょう。
—
1. JavaScriptのコードは「実行される前」に何を見ているのか?
私たちが普段何気なく書いているJavaScriptのコードは、実はそのまま上から順に解釈されながら実行されているわけではありません。
JavaScriptエンジン(V8など)は、コードを実行する前に「パース(解析)」という非常に重要なフェーズを挟みます。このパースの段階で、コードのテキストはAST(Abstract Syntax Tree:抽象構文木)という、コンピュータが理解しやすいツリー状のデータ構造に変換されます。
【ソースコード】
↓ (レキサー:字句解析)
【トークンの列】
↓ (パーサー:構文解析)
【AST(抽象構文木)】 ──★ここで「巻き上げ(Hoisting)」の土台が作られる!
パーサーがこのASTを組み立てる際、「どこに変数があるか」「どこに関数があるか」をあらかじめスキャンして、実行コンテキスト(変数環境)に登録します。これが、世間でよく言われる「巻き上げ(Hoisting)」の正体です。
コードが1行目すら実行される前に、すでに変数や関数の「席」がメモリ上に確保されている。だから、定義より上の行からその名前を参照できてしまうんですね。
—
2. 関数宣言と変数宣言の衝突:ASTはどう優先順位を決めるのか?
さて、ここからが本題です。もし、同じ名前の「変数(`var`)」と「関数宣言」がコード内に混在していたら、JavaScriptエンジンは一体どちらを優先してメモリに登録するでしょうか?
実際のコードで実験してみましょう。以下のコードを見てください。
// さあ、magicという名前の変数と関数、どちらが勝つでしょうか?
console.log(magic);
var magic = “私は変数です!”;
function magic() {
return “私は関数です!”;
}
console.log(magic);
他の言語に慣れていると、「`var magic` で上書きされるのかな?」あるいは「エラーになるのでは?」と思うかもしれません。しかし、これを実行すると、出力結果はこうなります。
[Function: magic]
私は変数です!
なんと、1回目の `console.log(magic)` では、`undefined` でもエラーでもなく、関数がそのまま出力されています。そして、代入文(`magic = “私は変数です!”`)が通過した後の2回目の出力では、見事に文字列に書き換わっています。
なぜこのような現象が起きたのでしょうか? これこそが、AST構築時における巻き上げの優先順位のルールを示しています。
パーサーの頭の中をのぞいてみよう
パーサーがコードをスキャンしてASTや変数環境を構築するとき、以下の厳格な優先順位(ルール)に従って名前を登録していきます。
1. 関数宣言(Function Declaration)は、最も優先度高く最上部に登録される。
2. 変数宣言(`var`)は、その後に登録されるが、すでに同名の関数が登録されている場合は「上書きを無視(何もしない)」される。
3. ただし、コードの実行フェーズに入ったときの代入(`= `)は、通常通り後から値を書き換える。
つまり、先ほどのコードの裏側では、エンジンはこのような順序でメモリの準備をしています。
// — 【エンジンが裏側で行っていることのイメージ】 —
// 1. まず関数宣言が登録される
function magic() {
return “私は関数です!”;
}
// 2. var magic も登録されようとするが、
// すでに同名の “magic” 関数が存在するため、宣言の登録は無視される
// (var magic; と書いてあるのと同じ扱いになるが、関数は消えない)
// — 【ここから実行フェーズ】 —
console.log(magic); // 上で登録された関数が出力される!
magic = “私は変数です!”; // ここで初めて値が上書きされる
console.log(magic); // 文字列が出力される
これが、関数宣言と変数宣言が衝突したときの真実のメカニズムです。関数は「特等席」を最初から確保できるため、`var` よりも強い権限を持っているとイメージすると分かりやすいですね。
—
3. 初学者が陥りがちな罠と、モダンな解決策
ここまで聞くと、「なるほど、`var` と関数が被っても関数が勝つんだな」と理解できたと思いますが、実務の現場ではこのような書き方は絶対にご法度です。
なぜなら、コードを読む人間(チームメンバーや未来の自分)にとってあまりにも直感的ではなく、バグの温床になるからです。「あれ、この変数、どこから関数として降ってきたんだっけ?」と混乱してしまいます。
現代のJavaScript(ES6以降)でのベストプラクティス
幸いなことに、現代のJavaScript開発では、`var` を使う必要性はほぼゼロになりました。代わりに `let` や `const` を使います。
さらに、変数に関数を代入する「関数式(Function Expression)」を使うことで、巻き上げの混乱を完全に排除できます。
// NGな例(var と関数宣言の混合はバグの元)
// 意図しない上書きや巻き上げによるバグを引き起こします
// OKな例(const とアロー関数 / 関数式を使用する)
const getGreeting = () => {
return “こんにちは、モダンなJavaScriptへようこそ!”;
};
console.log(getGreeting()); // 予測通り、この行に到達してから初めて使える
`let` や `const` で宣言された変数も実は巻き上げ自体は行われています(「Temporal Dead Zone:一時的死滅領域」と呼ばれる特殊なエリアに入ります)。しかし、`var` のように勝手に `undefined` で初期化されたり、関数宣言とこっそり競合したりすることはありません。宣言される前にアクセスしようとすると、JavaScriptエンジンが即座に `ReferenceError`(参照エラー)を投げて教えてくれます。
エラーを隠蔽するのではなく、「おかしなコードがあったらすぐにエラーで止めてくれる」。これがモダンなJavaScriptランタイムの優しさであり、堅牢なアプリケーションを作るための鉄則です。
—
まとめ
- ASTとパーサー: JavaScriptのコードは実行前にパースされ、ASTが作られる段階で変数や関数の位置(巻き上げ)が決定される。
- 優先順位のルール: 同名の識別子がある場合、関数宣言が最優先で登録され、`var` 宣言はそれに上書きされない。
- 実務での知見: 複雑な巻き上げの挙動に依存するコードは書かず、`const` と `let` を適切に使い分けて予測可能なコードを書くのがプロの流儀。
ここをクリアできれば、もう変数のスコープや巻き上げで迷うことはありません。自信を持って、次のモダンなフロントエンドの技術へステップアップしてくださいね!