こんにちは!JavaScriptのコアな仕組みへようこそ。
今回は、フロントエンドからNode.jsのバックエンドまで、あらゆるJSエンジニアが避けて通れない「変数の巻き上げ(Hoisting)と関数宣言の優先順位」について、V8エンジンの裏側の動きまで踏み込んで一緒に紐解いていきましょう。
他のプログラミング言語からやってきた開発者ほど、「あれ、なんでこのコードがエラーにならないんだ?」「変数より関数が勝つのはなぜ?」と混乱しがちです。でも大丈夫です。ここをクリアすれば、JavaScriptのコードが実行される頭の中のシミュレーション能力が劇的に跳ね上がり、バグ知らずのエンジニアに一歩近づけますよ。それでは、温かくロジカルに解説していきますね。
—
1. そもそも「巻き上げ(Hoisting)」って何だっけ?
JavaScriptのエンジン(V8など)は、コードを上から1行ずつ上から下に「ただ実行している」わけではありません。実は、コードを実際に実行する前に、「コンテキストの生成(Creation Phase)」という事前準備のフェーズを必ず挟んでいます。
この事前準備フェーズで、エンジンはコード全体をスキャンし、どこで変数や関数が宣言されているかをあらかじめ見つけて、スコープの先頭に「引き上げる」ような動作をします。これが「巻き上げ」の正体です。
まずは、基本の「変数」と「関数」の巻き上げの挙動を軽くおさらいしておきましょう。
// 変数宣言の巻き上げ(varの場合)
console.log(userName); // エラーにならず、”undefined” が出力される!
var userName = “Taro”;
console.log(userName); // “Taro” が出力される
なぜ1回目の `console.log` でエラー(ReferenceError)にならずに `undefined` になるのか?
それは、事前スキャン時にエンジンが `var userName;` という宣言だけをスコープの最上部に引き抜き、初期値として `undefined` を割り当てておくからです。
—
2. 本題:同じ名前の「変数」と「関数」がぶつかったらどうなる?
ここからが今回のメインディッシュです。
もし、同じスコープ内に同じ名前の変数と関数宣言が同時に存在していたら、JavaScriptはどちらを優先すると思いますか?
実際のコードで試してみましょう。
// さあ、このコードを実行すると何が出力されるでしょうか?
console.log(greeting);
var greeting = “こんにちは、変数です!”;
function greeting() {
return “こんにちは、関数です!”;
}
console.log(greeting);
直感的には、「あとから代入している `var greeting = …` が勝つんじゃないの?」あるいは「エラーになるのでは?」と思うかもしれませんよね。
しかし、これを実行すると、結果はこうなります。
// 実行結果
[Function: greeting] <-- 1回目の出力:なんと「関数」が出力される!
こんにちは、変数です! <-- 2回目の出力:後半で上書きされた文字列が出力される
不思議ですね。なぜ1回目の `console.log(greeting)` では、変数ではなく関数が勝ったのでしょうか?
その答えは、JavaScriptエンジンがAST(抽象構文木)を構築し、スコープのメモリ空間を初期化する際の「厳格な優先順位ルール」に隠されています。
—
3. V8エンジン内部のAST生成フェーズと優先順位のカラクリ
JavaScriptエンジンがコードを実行する直前、以下のようなステップを踏んでメモリ(環境レコード)を構築しています。
1. 関数宣言(Function Declaration)の登録
エンジンはコード全体をスキャンし、最初に見つけた関数宣言を名前ごと、関数オブジェクトの参照を含めて丸ごとメモリに登録します。この瞬間、その名前空間の主導権は完全に関数が握ります。
2. 変数宣言(Variable Declaration – var)の登録
次に変数宣言をスキャンしますが、すでに同名の関数が登録されている場合、「すにでに同名の識別子が存在するので、上書きせずに無視(スキップ)する」という仕様になっています。
つまり、事前準備が終わった段階でのメモリのイメージは、次のような状態になっています。
// エンジン内部の事前準備完了時のイメージ
function greeting() {
return “こんにちは、関数です!”;
}
// var greeting は「すでに同名があるから」スキップされる!
// (let や const の場合はまた別の挙動になりますが、それは後ほど)
// — ここから実行フェーズ(Execution Phase) —
console.log(greeting); // メモリに残っているのは関数なので、関数が出力される
greeting = “こんにちは、変数です!”; // ここで初めて値が文字列に「代入(上書き)」される
console.log(greeting); // 変数として代入された文字列が出力される
関数は「宣言」の時点で中身のロジックごとメモリに固定されるのに対し、`var` による変数は事前準備段階では単なる「枠(`undefined`)」でしかなく、しかも同名関数には勝てない。これが、「関数が変数に勝つ」メカニズムの正体です。
—
4. モダンJS(let / const)における挙動と、陥りやすい罠
「なるほど、じゃあ最近のモダンな `let` や `const` を使ったらどうなるの?」と思ったあなたは、非常に鋭い視点を持っています。
実は、`let` や `const` を使って同じことをやろうとすると、先ほどとは全く違う挙動、あるいは文法エラーに直面します。
// let と関数が同名の場合
console.log(data); // ここはどうなる?
let data = “モダンな変数”;
function data() {
return “関数”;
}
これを実行すると、なんと次のようなエラーが発生します。
SyntaxError: Identifier ‘data’ has already been declared
(構文エラー: 識別子 ‘data’ はすでに宣言されています)
`let` や `const` は、同じスコープ内での重複した宣言を一切許さないという厳格なルールを持っています。そのため、事前スキャン(ホイスティング)のフェーズで「おっと、同名の識別子がもうあるぞ」と検知した瞬間に、容赦なくSyntaxErrorを吐き出して実行を止めてくれます。
初学者が陥りがちなアンチパターン
現場の開発現場や、古いレガシーコードを改修している時によく見かけるのが、意図せず同じ名前を変数と関数につけてしまい、バグを生むケースです。
// 良くない例:変数名と関数名がコンフリクトしている
let user = fetchUserData();
function user() {
// 何らかの処理…
}
// 意図したデータ処理のつもりが、予期せぬ挙動やエラーを引き起こす原因に
現代のJavaScript開発では、ESLintなどの静的解析ツールが標準装備されているため、こうした名前の重複は事前に検知してくれますが、根本的なランタイムの仕組みを知っておくことで、デバッグのスピードが段違いに上がります。
—
まとめ:ここをクリアすれば、JSの基本はバッチリ!
いかがでしたでしょうか? 今回のポイントをギュッと凝縮してまとめます。
- 巻き上げ(Hoisting)は、コード実行前の事前準備フェーズ(メモリの初期化)で発生する。
- 同じスコープ内に同名の「変数(`var`)」と「関数宣言」がある場合、関数宣言が優先され、変数の巻き上げによる初期化は無視される。
- `let` や `const` を使った場合は、重複宣言自体が許されないため、SyntaxErrorになる。
- 変数と関数には適切な名前をつけ、名前の衝突(コンフリクト)を避ける設計がプロフェッショナルへの第一歩。
この仕組みを頭の片隅に置いておくだけで、ブラウザやNode.jsが裏側でどう動いているのかが手に取るようにわかるようになります。
ここをクリアできたあなたなら、変数やスコープ周りで迷うことはもうありません。自信を持ってモダンなJS開発を楽しんでいきましょう!