こんにちは!JavaScriptの世界へようこそ。
フロントエンドからNode.jsの深層までコードを書き続けてきた私と一緒に、今日はJavaScriptの隠れた挙動、「巻き上げ(Hoisting)」の真実を紐解いていきましょう。
他の言語からやってきた開発者や、プログラミングを学び始めたばかりの方が、一番最初に「おや?」とつまずくのが、この変数や関数のスコープと巻き上げの仕組みですよね。
「宣言する前になぜかコードが動く…?」「いや、逆に `undefined` になったりエラーになったりする…?」
そんなモヤモヤを、今日ここで完全にクリアにしちゃいましょう。ここを突破すれば、あなたのJavaScriptのコードはぐっと洗練され、バグの少ない美しいものになりますよ。
—
1. そもそも「巻き上げ(Hoisting)」って何?
JavaScriptのコードは、私たちが書いた上から順番に1行ずつ実行されている……ように見えますよね。実はその裏で、JavaScriptエンジン(ブラウザならV8など)は、コードを実行する前に「準備運動」をしています。
この準備運動の段階で、エンジンはコード全体をスキャンし、変数や関数の宣言をスコープの先頭に「引き上げた(ように扱った)」状態を作ります。これが世間で言われている「巻き上げ(Hoisting)」の正体です。
ですが、ここで一つ大きな誤解があります。
「コードの文字自体が物理的に上に移動しているわけではない」ということです。
移動しているのではなく、V8エンジンが実行コンテキスト(Execution Context)を作る際、メモリ上に変数や関数の場所をあらかじめ確保している、というのが正確なメカニズムなんですね。
—
2. 実行コンテキストの裏側:何が起きているのか?
JavaScriptのコードが実行される前段階(Creation Phase:生成フェーズ)では、メモリ空間(Variable Object / Lexical Environment)の構築が行われます。
ここで、関数宣言と変数宣言(`var`、`let`、`const`)は、それぞれ扱われ方が全く異なります。これが、私たちが混乱する原因のすべてです。
優先順位のルール
メモリの割り当てには厳格な優先順位があります。
1. 関数宣言(Function Declaration):一番乗りでメモリにガッツリ確保され、中身の関数オブジェクトまで完全に登録されます。
2. 変数宣言(`var`):メモリに場所は確保されますが、値は `undefined` で初期化されます。
3. 変数宣言(`let` / `const`):メモリには確保されますが、初期化はされません(Temporal Dead Zone:一時的死海域に置かれます)。
それでは、具体的なコードでこの優先順位と動きを覗いてみましょう。
—
3. 実践コードで見る「関数と変数のガチンコ優先順位」
次のコードを見てください。少し意地悪な書き方ですが、巻き上げの本質を理解するのに最高の教材です。
// さあ、このコードを実行すると何が出力されるでしょうか?
console.log(myThing); // ① ここは何が表示される?
var myThing = “変数だよ”;
function myThing() {
return “関数だよ”;
}
console.log(myThing); // ② ここは何が表示される?
他の言語を知っているとなおさら、「関数が先? 変数が先? え、エラーになるの?」と迷うところですよね。
実際に頭の中で脳内トレースをしてみてください。
正解の出力結果はこうなります。
[Function: myThing] // ① の結果
変数だよ // ② の結果
なぜ、変数 `myThing` を宣言して代入しているにもかかわらず、最初の `console.log` では関数が出力されたのでしょうか?
メモリの動きを実況中継!
JavaScriptエンジンがこのコードを読み込んだとき、実行前の準備フェーズで以下のようなことが起きています。
1. 関数宣言の最優先登録
エンジンはコード全体を見渡し、「お、`myThing` という名前の関数宣言があるな」と気づきます。そして、メモリ上に `myThing` という空間を作り、関数の中身丸ごとをそこに格納します。
2. 変数宣言の登録(ただし控えめ)
次に `var myThing` という変数宣言を見つけます。しかし、すでにメモリには同名の `myThing`(関数)が登録されています。「おっと、すでに同じ名前があるから、変数の宣言は一旦スルー(上書きしない)しておこう」となるわけです。
そのため、1つ目の `console.log(myThing)` が実行された時点では、変数が “変数だよ” に書き換わる前であり、メモリに残っていた「関数」が勝者として出力されたのです。
その後、コードが上から順番に実行されていくフェーズ(Execution Phase)に突入すると、2行目で `myThing = “変数だよ”` という値の代入が実行されます。だからこそ、2つ目の `console.log` では無事に「変数だよ」と表示されるわけですね。
※もし関数宣言ではなく、関数式(`var myThing = function() {}`)で書いていたら、結果は全く異なり、最初は `undefined` になります。ここ、めちゃくちゃテストに出る(実務でハマる)ポイントですよ!
—
4. `let` と `const` が登場したモダンな世界の巻き上げ
「じゃあ、モダンな `let` や `const` も同じように巻き上げられるの?」という疑問が湧きますよね。
結論から言うと、`let` や `const` も巻き上げは起きています。
ただし、`var` とは決定的な違いがあります。それは「初期化されない(Temporal Dead Zoneに入る)」という点です。
// モダンな書き方をした場合
console.log(modernVar); // ⚠️ ここでエラーになります!
let modernVar = “新しい変数”;
これを実行すると、JavaScriptは優しく教えてくれません。容赦なく以下のエラーを投げつけてきます。
ReferenceError: Cannot access ‘modernVar’ before initialization
(初期化する前に ‘modernVar’ にアクセスすることはできません)
「あれ? 巻き上げられてるなら `undefined` になるんじゃないの?」と思いますよね。
ここが重要です。`let` や `const` は、メモリには「ここに変数が存在すること」は登録される(=巻き上げられている)のですが、実際にその行にコードが到達して初期化されるまで、アクセスを一切禁止する一時的死海域(TDZ:Temporal Dead Zone)というシールドで守られています。
これにより、「宣言する前にうっかり変数を使ってしまい、意図しないバグを生む」という `var` 時代の悪夢から私たちを救ってくれているのです。
—
5. 先輩からのアドバイス:どう書くのがベスト?
ここまで、JavaScriptの裏側のメカニズムである「巻き上げ」と「優先順位」について解説してきました。
「仕組みが分かって面白かった!」と思っていただけたなら嬉しいのですが、実務の現場における鉄則はたった一つです。
> 「巻き上げに頼ったコードを書かないこと」
これに尽きます。
コードは常に、上から下へ、論理的で自然な流れで読めるのがベストです。
- 変数や定数は、使う前に必ず宣言する(できれば `let` よりも `const` を使い、再代入を防ぐ)。
- 関数は、利用する前に定義するか、あるいは関数式・アロー関数を使って綺麗にスコープを整理する。
JavaScriptエンジンの挙動(V8のメモリ管理や実行コンテキスト)を頭の中でイメージできるようになると、エラーが出たときも「あ、今メモリのコンテキストはこういう状態だな」と秒速で原因が特定できるようになります。
ここをクリアしたあなたなら、もうJavaScriptの基本文法で迷子になることはありません。
さあ、自信を持って次のコードを書きに行きましょう!