こんにちは!普段はフロントエンドからNode.jsのバックエンド、そしてV8エンジンの挙動までを追いかけながら、日々コードを書いています。
JavaScriptを学び始めると、必ずと言っていいほどぶつかる壁がありますよね。それが「巻き上げ(Hoisting)」という現象です。
「変数を作る前にconsole.logを書いたら、エラーにならずに `undefined` が出てきた……これってバグなの?」
そんな疑問を持ったことはありませんか?
実はこれ、JavaScriptの仕様が裏側で行っている「ある特別な準備作業」が原因なんです。ここをクリアすれば、JavaScriptのコードが脳内でスッキリとトレースできるようになり、一気にコードの理解が深まりますよ。さあ、一緒にその正体を紐解いていきましょう!
—
1. JavaScriptが動く仕組み:2つのステップを知ろう
JavaScriptのエンジン(ブラウザならChromeのV8など)は、私たちが書いたコードを上から順番にただ垂れ流しで実行しているわけではありません。
コードを実行する前に、必ず2つのフェーズ(段階)を踏んでいます。ここが今日の最重要ポイントです。
1. コンパイルフェーズ(準備・スキャン段階)
- コードを実行する「直前」に、エンジン全体をサーッと見渡します。
- そして、`var` や `let`、`const`、そして `function` などの「変数の宣言」や「関数の定義」を見つけて、メモ帳(実行コンテキスト)に名前を書き留めていきます。
2. 実行フェーズ(上からの実行段階)
- 準備が終わったあとで、改めてコードを1行目から上から順番に実行していきます。
この「コンパイルフェーズ」において、変数の宣言が、そのスコープ(有効範囲)の最上部に引き上げられたかのように扱われる現象こそが、世間一般で「巻き上げ(Hoisting)」と呼ばれているものの正体です。物理的にコードの文字が上へワープしているわけではなく、「実行前に名前の登録が済んでいる」という状態を指しているんですね。
—
2. `var` の巻き上げ:なぜ `undefined` になるのか?
言葉だけだとイメージしにくいので、実際のコードを見てみましょう。まずは昔ながらの `var` を使った例です。
// 宣言より前なのにエラーにならない?
console.log(greeting); // 出力結果: undefined
var greeting = “こんにちは、JavaScriptの世界へ!”;
console.log(greeting); // 出力結果: こんにちは、JavaScriptの世界へ!
不思議ですよね。「まだ `greeting` なんて作ってないよ!」というタイミングで `console.log` を呼んでいるのに、エラー(ReferenceError)ではなく `undefined` が返ってきます。
裏側で何が起きているのか、JavaScriptエンジンの頭の中を覗いてみましょう。
裏側の動きをシミュレーション
// 【コンパイルフェーズ(準備)】
// エンジンはコード全体をスキャンし、「var greetingがあるな」と記憶します。
// この時、varで宣言された変数は自動的に「undefined」という初期値でメモに登録されます。
var greeting;
// 【実行フェーズ(ここから実際にコードが上から動く)】
console.log(greeting); // メモには「undefined」が入っているので、そのままundefinedが出力される
greeting = “こんにちは、JavaScriptの世界へ!”; // ここで初めて値が代入される
console.log(greeting); // 代入された文字列が出力される
つまり、`var` の巻き上げが起きると、「名前の登録」と同時に「`undefined`という仮の初期値」がセットされるため、エラーにならずに `undefined` が表示されるというわけです。
—
3. モダンな `let` と `const` の世界:エラーになる理由
「じゃあ、現代のJavaScriptで主流の `let` や `const` でも同じことが起きるの?」と思いますよね。
実は、`let` や `const` でも巻き上げ(コンパイルフェーズでの変数名の事前登録)自体は起きています。しかし、挙動が `var` とはまったく異なります。実際に試してみましょう。
// letで宣言する前にアクセスしてみる
console.log(secretMessage); // ここでエラーが発生!
let secretMessage = “秘密のメッセージです”;
これを実行すると、ブラウザのコンソールやNode.jsには次のような恐ろしいエラーが表示されます。
> ReferenceError: Cannot access ‘secretMessage’ before initialization
> (初期化する前に ‘secretMessage’ にアクセスすることはできません)
`undefined` にすらならず、ガッツリとエラーで止まりましたね。
「一時的デッドゾーン(TDZ)」という厳格なガード
`let` や `const` がこのような挙動をするのは、バグを未然に防ぐためのモダンな設計思想によるものです。
コンパイルフェーズで `let` や `const` の変数も事前登録されますが、`var` のように勝手に `undefined` で初期化されることはありません。コードが実際にその行(宣言の行)を通過して「初期化」されるまでの間、その変数は「一時的デッドゾーン(Temporal Dead Zone: TDZ)」という、アクセス禁止のエリアに置かれます。
// 【コンパイルフェーズ】
// let secretMessage; (名前の登録はされるが、値は入れられない。TDZ突入)
// 【実行フェーズ】
console.log(secretMessage);
// ❌ まだ宣言の行を通過していない(TDZ内)ため、JavaScriptが「おい、まだ準備できてないのに触るな!」とエラーを投げる
let secretMessage = “秘密のメッセージです”; // ⭕️ ここを通過した瞬間にTDZが解除され、使えるようになる
この厳格さのおかげで、「意図せず `undefined` のまま計算に使ってしまい、気づきにくいバグを生む」という `var` 時代によくあったトラブルを綺麗に回避できるようになったのです。
—
4. 関数宣言の巻き上げは「中身ごと」持ち上がる
変数の話をしてきましたが、実は「関数」の巻き上げは少し性質が違います。
関数を定義する方法にはいくつかありますが、王道の「関数宣言(Function Declaration)」の場合、なんと関数の中身(本体)ごとコンパイルフェーズでメモリにロードされます。
// 宣言より前に関数を呼んでも……?
sayHello(); // 出力結果: “こんにちわ!”
// 関数宣言
function sayHello() {
console.log(“こんにちわ!”);
}
なんと、コードの定義位置よりも上で関数を呼び出しているにもかかわらず、何事もなかったかのように動きます。
これは、コンパイルフェーズの段階で「`sayHello` という名前の関数で、中身の処理はこれこれこういうものだ」という実体がすべてメモリ上に構築されるためです。そのため、コードのどこからでもその関数を呼び出すことができます(※ただし、関数を代入する「関数式 `const sayHello = function() {}`」の場合は、変数と同じルールになるため巻き上げによるエラーが起きます)。
—
まとめ:今日から使える実践的なアドバイス
巻き上げの仕組み、スッキリ理解できたでしょうか? 最後に、実務や日々のコーディングで意識すべきポイントをまとめます。
- `var` は使わない: 意図しない `undefined` やスコープの漏れを引き起こす原因になるため、現代の開発では原則として `let` と `const` を使いましょう。
- 変数は「使う前」に宣言する: `let` や `const` の一時的デッドゾーンの仕組みを理解していれば、「変数はスコープの最上部で宣言してから使う」というクリーンな書き方が自然と身につくはずです。
ここをクリアできれば、JavaScriptのエンジンの動きが頭の中でハッキリとイメージできるようになります。コードの挙動に迷わなくなる第一歩ですね。
今日の知識を武器に、さらにモダンで美しいJavaScriptコードを書いていきましょう!