こんにちは!フロントエンドからNode.jsの内部挙動まで、日夜JavaScriptと向き合っているシニアアーキテクトです。
今回は、JavaScriptの学習を始めて多くの人が最初に「おや?」とつまずく、変数と関数の「巻き上げ(Hoisting)」の優先順位についてお話しします。
「同じ名前の変数と関数を宣言したら、一体どうなるの?」
「エラーになる? それとも上書きされる?」
そんな疑問を持ったことはありませんか? ここをクリアすれば、JavaScriptのコードが実行される頭の中のシミュレーション(脳内トレース)が完璧にできるようになりますよ。それでは、エンジニアの扉を開いていきましょう!
—
1. 巻き上げ(Hoisting)の基本をおさらいしよう
JavaScriptのコードは、私たちが書いた上から順番に1行ずつ実行されている…ように見えて、実はその裏で「実行する前の準備運動(コンテキストの生成)」という大切なフェーズを行っています。これが、V8などのJavaScriptエンジンが裏側で行っている AST(抽象構文木)の生成とスコープの解析 です。
この準備フェーズの段階で、エンジンはコード内にある `var` や `function` などの宣言を、スコープの一番上に「ググッと引き上げる」ような処理をします。これが「巻き上げ」と呼ばれる現象ですね。
まずは、基本の形を確認してみましょう。
// 変数宣言の巻き上げの例
console.log(message); // 出力はどうなるでしょう?
var message = “こんにちは、JavaScript!”;
console.log(message); // こちらはもちろん「こんにちは、JavaScript!」
他のプログラミング言語(例えばJavaやC++など)から来た方なら、「えっ、宣言する前に `console.log` を呼んだらエラー(ReferenceError)になるのでは?」と思いますよね。
しかし、JavaScriptの `var` では、エラーにはならず `undefined` と表示されます。これは、「変数 `message` の名前だけが、スコープの先頭に巻き上げられたから」です(ただし、代入は元の場所で行われるため、値は `undefined` になります)。
—
2. 本題:同じ名前の「関数」と「変数」がぶつかったら?
さて、ここからが本題です。もし、同じスコープ内に同じ名前の「関数宣言」と「変数(var)宣言」が同時に存在していたら、一体どちらが優先されてメモリ上に生き残るのでしょうか?
実際のコードを見てみましょう。
// さあ、このコードを実行すると何が起きるでしょうか?
console.log(myVariable);
var myVariable = “私は変数です!”;
function myVariable() {
return “私は関数です!”;
}
console.log(myVariable);
頭の中で、エンジンになったつもりで解析してみてください。
「変数が勝つのか? いやいや、関数のほうが強そう?」
正解の挙動を、実行結果とともに見てみましょう。
// 1回目の console.log の出力
// [Function: myVariable] (なんと、関数が出力される!)
// 2回目の console.log の出力
// “私は変数です!” (あれっ、後半では変数に上書きされている!)
不思議ですよね。なぜ1回目の時点では、変数の `undefined` ではなく、関数がそこに存在しているのでしょうか? ここに、JavaScriptのAST解析における「明確なルール」が隠されています。
—
3. なぜ関数が勝つのか? AST解析と優先順位のルール
JavaScriptエンジンがコードを読み込むとき、スコープ内への登録には厳格な優先順位のルールが存在します。
決定論的なルールは以下の通りです。
1. 関数宣言(Function Declaration)は、変数宣言よりも優先的にスコープの最上部に登録されます。
2. 変数宣言(`var`)は、後からその場所に追加されますが、すでに同名の関数が存在する場合、関数を上書きすることはありません(つまり、変数の「宣言」自体が無視されるような形になります)。
3. ただし、コードが上から順番に実行されていくフェーズ(実行コンテキストの実行段階)で、代入式(`myVariable = “私は変数です!”`)に到達した瞬間、その変数の値で中身が上書きされます。
つまり、先ほどのコードの裏側では、エンジンによって次のように解釈(再構築)されています。
// — JavaScriptエンジン内部の脳内イメージ —
// 1. まず、関数宣言が最優先で巻き上げられる
function myVariable() {
return “私は関数です!”;
}
// 2. var 宣言は同名ですでに存在するので、無視される(巻き上げられない)
// var myVariable; (← スキップされる)
// — ここから通常の実行フェーズ —
console.log(myVariable); // ここではすでに「関数」が入っている!
myVariable = “私は変数です!”; // ここで初めて「文字列」に代入し直される
console.log(myVariable); // ここでは「文字列」が出力される
この仕組みを知っていれば、「なぜ最初に関数が出てくるのか」「なぜ途中から文字列に変わるのか」が完全に理解できますよね。
—
4. 現代の開発におけるベストプラクティス:トラブルを防ぐ極意
ここまで「関数と変数の巻き上げ優先順位」というコアな仕組みを解説してきましたが、実務の現場において、この挙動を意図してコードを書くことは絶対に避けるべきです。
なぜなら、同じ名前の変数と関数を混在させると、コードの可読性が著しく下がり、バグの温床になるからです(これを「シャドーイング」や名前の衝突と呼びます)。
モダンなJavaScript開発(ES6以降)では、以下の鉄則を守ることで、このような巻き上げの混乱から完全に解放されます。
① `var` を使わず、`let` や `const` を使う
`let` や `const` にも巻き上げは発生しますが、初期化される前にアクセスすると `ReferenceError`(参照エラー)を発生させてくれます。これにより、意図しない `undefined` や予期せぬ挙動を防ぐことができます。
// let や const なら、宣言前にアクセスすると即座にエラーになり、安全!
console.log(modernVariable); // ReferenceError: Cannot access ‘modernVariable’ before initialization
let modernVariable = “安全です!”;
② 使う「前」に宣言と代入を済ませる
コードは常に「上から下へ」自然に読めるように書きましょう。関数を使うときは事前に定義し、変数を使うときも必ず宣言と初期化を済ませてからアクセスする癖をつけることが、プロとしての一番の近道です。
—
まとめ
いかがでしたでしょうか?今回は少しディープなV8エンジンのAST解析と巻き上げの優先順位について紐解いてみました。
- 関数宣言は、変数宣言よりも優先してスコープのトップに巻き上げられる。
- そのため、実行開始直後は同名の変数が存在していても「関数」が勝つ。
- しかし、実行フェーズで値の代入(`=`)が通ると、そこから先は変数の中身に上書きされる。
この仕組みをクリアに理解していれば、他人が書いた複雑なレガシーコードに遭遇しても、頭の中で正確に挙動をトレースしてバグを瞬時に見抜くことができますよ。
ここをマスターしたあなたなら、もうJavaScriptの変数スコープで迷うことはありません。自信を持って次のステップへ進んでくださいね!