こんにちは!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の変数スコープや実行コンテキストの基本はもうバッチリマスターできていますよ!自信を持って、次のステップへ進んでいきましょう。