こんにちは!JavaScriptのコアな仕組みへようこそ。
フロントエンドの画面描画からNode.jsのサーバーサイドまで、JavaScriptという言語の鼓動を感じながら日々コードを書いていると、「あれ、変数や関数って、どうして宣言する前に使えちゃうんだろう?」と不思議に思ったことはありませんか?
他のプログラミング言語、例えばJavaやC++、Swiftなどを触ったことがある方なら、「宣言していない変数や関数を呼び出すなんてエラーになるのが当然!」と思われるはずです。しかし、JavaScript(特に古き良き `var`)の世界では、コードが上から下に実行されるより前に、JavaScriptエンジンがこっそり準備を済ませてしまう「巻き上げ(Hoisting)」という不思議な現象が起きます。
よく「`var`の巻き上げは言語の設計ミスだ、バグだ」なんて言われがちですが、実はこれ、JavaScriptのパーサー(解析器)がコードを安全に実行するための厳密な「仕様」なんですよ。
今回は、この巻き上げのメカニズムを、V8などのJavaScriptエンジンが頭の中でやっている「AST(抽象構文木)の解析順序」と「Environment Record(環境レコード)」の視点から、優しく紐解いていきましょう。ここをクリアすれば、JavaScriptの基本はバッチリマスターできますよ!
—
1. まずはおさらい:`var`の巻き上げってどうなるの?
まずは、実際にコードがどう動くのかを見てみましょう。他の言語から来た方にとっては驚きの挙動ですが、JavaScriptではこれが正常に動きます。
// まだ宣言していないのに、エラーにならずに「undefined」と表示される!
console.log(greeting); // 出力: undefined
var greeting = “こんにちは、JavaScriptの世界へ!”;
console.log(greeting); // 出力: こんにちは、JavaScriptの世界へ!
なぜ、1回目の `console.log(greeting)` でエラー(ReferenceError)にならずに `undefined` になるのでしょうか?
それは、JavaScriptエンジンがコードを実行する前に、「コード全体から `var` や `function` を先に見つけて、メモリの準備(登録)を先にしておく」という下準備(フェーズ)を行っているからなんです。
これを人間が読みやすいようにコードの書き換えで表現すると、JavaScriptエンジンの中では次のようなことが起きています。
// — JavaScriptエンジンが脳内でやっていること(イメージ) —
// 1. var宣言がスコープの最上部に「巻き上げられる」
var greeting;
// 2. 通常の実行が上からスタート
console.log(greeting); // まだ値を入れていないので「undefined」
greeting = “こんにちは、JavaScriptの世界へ!”;
console.log(greeting); // 値が入ったので「こんにちは、JavaScriptの世界へ!」
変数の中身(代入)はそのままで、「宣言(変数の名前)」だけがコードの先頭に持ち上げられる。これが「巻き上げ(Hoisting)」の正体です。
—
2. エンジンの頭の中:AST(抽象構文木)とEnvironment Record
では、JavaScriptエンジンは一体どうやってこれを実現しているのでしょうか? 少しだけ内部の仕組み(ランタイムの世界)をのぞいてみましょう。
私たちが書いたJavaScriptのテキストコードは、そのまま実行されるわけではありません。まずパーサーというプログラムによって、コードの構造をツリー状に解析したAST(Abstract Syntax Tree / 抽象構文木)というデータ構造に変換されます。
パーサーの「2度読み」の秘密
JavaScriptエンジン(V8など)は、コードを実行する前に次のようなステップを踏んでいます。
1. 構文解析(Parsing)フェーズ
エンジンがファイル全体を読み込み、ASTを構築します。この時、スコープ(関数やグローバル)の中にどんな `var` があり、どんな `function` があるかをくまなくチェックします。
2. Environment Record(環境レコード)への登録
見つけた変数や関数の名前を、メモリ上の「変数 environment」に登録します。
- `var` の場合:名前を登録し、初期値として `undefined` をバインドします。
- 関数宣言(Function Declaration)の場合:名前だけでなく、関数の中身(本体)丸ごとメモリに登録します!
3. 実行(Execution)フェーズ
上から順にコードが実際に実行されていきます。
この仕組みを知ると、関数宣言が変数よりもさらに強力に巻き上げられる理由が分かります。
—
3. 変数と関数で違う!「巻き上げられ方」の決定的な差
`var` による変数の巻き上げは「名前と `undefined` の登録」にとどまりますが、関数宣言(Function Declaration)は、本体のコードブロックごとメモリに登録されます。
次のコードを見てみましょう。
// 宣言するより前に関数を呼び出せる!
sayHello(); // 出力: 「関数が実行されました!」
function sayHello() {
console.log(“関数が実行されました!”);
}
これは、パーサーがASTを構築した段階で、`sayHello` という名前と、その中にある `console.log(…)` の処理全体がセットでEnvironment Recordに登録されているからです。そのため、コードのどの行からでも呼び出すことができます。
ここで注意!関数式(Function Expression)の場合
気をつけなければいけないのは、関数を変数に代入する形(関数式)で書いた場合です。
// エラーになる! TypeError: sayHi is not a function
sayHi();
var sayHi = function() {
console.log(“ハイ!”);
};
「あれ? 関数なのにエラーになるの?」と思いましたよね。
これは非常に重要なポイントです。このコードの正体は、「`var` による変数の巻き上げ」だからです。
先ほどのエンジンの頭の中を思い出してください。
- 巻き上げられるのは `var sayHi;` だけ。
- 1行目の実行時点では、`sayHi` の中身は `undefined` です。
- `undefined()` という関数ではないものを呼び出そうとしたため、「`sayHi is not a function`(関数ではありません)」というTypeErrorが発生するのです。
—
4. なぜこれが「バグ」ではなく「仕様」なのか?
歴史的背景として、Brendan Eich氏がJavaScriptをわずか10日足らずで設計した際、メモリの割り当てを効率的に行い、関数同士がどこで定義されていてもお互いを呼び出し合える(相互再帰などの柔軟な表現ができる)ようにするため、この巻き上げの仕組みが採用されました。
完璧とは言えないまでも、当時のWebブラウザという極限の環境でスクリプトを動かすためには、非常に合理的でパワフルな「仕様」だったのです。
とはいえ、現代のモダンな開発においては、`var` の巻き上げは予期せぬバグ(変数が意図せず `undefined` になるなど)の温床になり得ます。そのため、ES2015(ES6)以降では、巻き上げによる混乱を完全に防ぐために、新しい宣言方法である `let` と `const` が導入されました。
`let` と `const` の世界(TDZ:一時的死区間)
`let` や `const` も実は内部的には巻き上げが行われていますが、`var` のように `undefined` で初期化されません。
そのため、宣言の前にアクセスしようとすると、JavaScriptエンジンが強制的にエラーを投げます。これがTDZ(Temporal Dead Zone / 一時的死区間)と呼ばれる安全装置です。
// console.log(modernVar); // ReferenceError: Cannot access ‘modernVar’ before initialization
let modernVar = “安全でモダンな変数です”;
—
まとめ:今日のまとめとこれからの指針
いかがでしたでしょうか? 今回のポイントをギュッとまとめておきますね。
- 巻き上げ(Hoisting)とは:コードの実行前に、エンジンがASTを解析し、変数や関数の名前をEnvironment Recordに事前登録する「仕様」のこと。
- `var` の巻き上げ:名前が登録され、初期値として `undefined` が入る。そのため宣言前は `undefined` になる。
- 関数宣言の巻き上げ:関数名だけでなく中身のコードブロック丸ごと登録されるため、宣言前でも呼び出せる。
- 関数式の場合:中身は代入なので、実体は `var` の巻き上げと同じになり、エラー(TypeError)になる。
「なぜそんな動きをするのか」というエンジンの背景(ASTやパーサーの動き)まで理解しておくと、複雑なコードをデバッグする際や、新しい言語仕様を学ぶときにも、迷うことがなくなります。
基本のメカニズムをスッキリ理解できたあなたなら、もうJavaScriptの変数スコープで悩むことはありませんよ。明日からのコーディングを、さらに自信を持って楽しんでいきましょう!