こんにちは!日々の開発、本当にお疲れ様です。
JavaScriptを書き進めていく中で、「あれ、この変数、宣言する前になぜかアクセスできちゃうぞ?」あるいは逆に「`undefined`になって怒られた…」なんて経験はありませんか?
他のプログラミング言語、例えばJavaやC++などを触ってきた方からすると、コードの上から下へ流れるように実行されないJavaScriptのこの挙動は、まるで手品のように不思議に見えるかもしれません。世間ではこれを「変数の巻き上げ(Hoisting)」と呼びますが、実はこれ、コードが勝手に上へ移動しているわけではないんです。
今回は、JavaScriptの心臓部であるV8エンジンが、コードを読んでから実行するまでに裏側でどんなドラマを繰り広げているのか、その真実を一緒に覗いてみましょう!ここをクリアすれば、JavaScriptの変数とスコープの挙動はあなたの手のうちに収まりますよ。それでは、深掘りしていきましょう!
—
1. 「巻き上げ」という幻想:コードは移動していない
まず最初に、一番大切な誤解を解いておきましょう。「変数の巻き上げ」という言葉を聞くと、初心者の方はどうしても「JavaScriptのエンジンが、コンパイル時(実行前)にソースコードの変数宣言を勝手にファイルの先頭に物理的に移動させている」というイメージを持ちがちです。
ですが、そんなことは絶対に起こっていません。ソースコードのテキストそのものは1ミリも動いていないのです。
正体は移動ではなく、V8エンジンがコードを実行する前の「準備フェーズ(Creation Phase)」と「実行フェーズ(Execution Phase)」の分離にあります。
V8エンジンはコードをどう処理しているのか?
JavaScriptのエンジン(V8など)がスクリプトと出会ったとき、いきなり上から順番にコードを実行していくわけではありません。大まかに以下の2つのステップを踏んでいます。
1. 解析・準備フェーズ(Creation Phase): コード全体をざっと見渡して、どこにどんな変数や関数が宣言されているかを確認し、メモリ上に「場所」を確保する。
2. 実行フェーズ(Execution Phase): 準備が完了した後、上から下へコードを実際に実行していく。
つまり、巻き上げとは「コードが上にワープしている」のではなく、「エンジンが実行する前に、あらかじめ変数の席(メモリ)を用意している」という現象なわけです。これを知るだけでも、見え方がかなり変わってきますよね。
—
2. AST(抽象構文木)からEnvironment Recordへ
もう少しだけエンジニアの視点で、裏側のメカニズムを覗いてみましょう。V8エンジンは私たちが書いた文字列のコードをそのまま読んでいるわけではありません。
1. トークナイザー/パーサー: ソースコードを意味のある単位(トークン)に分解し、AST(Abstract Syntax Tree:抽象構文木)というツリー構造のデータに変換します。
2. スコープ解析: このASTを解析する過程で、どこからどこまでが同じ変数の有効範囲(スコープ)なのかを特定します。
3. Environment Record(環境レコード)への登録: 各スコープに対応する「Environment Record」というメモリ上のデータ構造に、見つかった変数名や関数名が登録されていきます。
この「Environment Recordへの登録」が、まさに巻き上げの正体です。`var`、`let`、`const`では、この登録のされ方と初期化のタイミングが異なるため、私たちが目にする挙動に違いが生まれます。
—
3. 三者三様!`var`、`let`、`const`の裏側の挙動
それでは、具体的なコードを見ながら、それぞれの変数が裏側でどう扱われているのかを確認していきましょう。ここが一番重要なポイントですよ!
パターンA:お騒がせな `var` の世界
まずは、昔ながらの `var` キー워드です。
// varを使ったお馴染みの挙動
console.log(greeting); // 1. エラーにならず、何が出るでしょう? -> undefined
var greeting = “こんにちは、世界!”;
console.log(greeting); // 2. “こんにちは、世界!”
裏側のメカニズム:
準備フェーズの段階で、V8エンジンは `var greeting` という宣言を見つけると、Environment Recordに `greeting` という名前を登録し、同時に自動で初期値として `undefined` を代入します。
そのため、1つ目の `console.log(greeting)` の時点ですでに変数の存在が知られており、エラーにならずに `undefined` が返ってくるのです。
パターンB:厳格で安全な `let` と `const` の世界
モダンなJavaScriptでは、基本的に `let` や `const` を使いますよね。これらは `var` とは全く異なる挙動をします。
// letを使った場合
try {
console.log(modernGreeting); // ここでどうなる?
} catch (error) {
console.log(“エラーをキャッチしました:”, error.message);
}
let modernGreeting = “モダンなJavaScriptへようこそ!”;
これを実行すると、次のようなエラーメッセージが出迎えてくれます。
`ReferenceError: Cannot access ‘modernGreeting’ before initialization`(初期化する前にアクセスできません)
裏側のメカニズム:
「あれ?エラーが出たってことは、巻き上げられてないの?」と思われるかもしれませんが、実は `let` や `const` も巻き上げは起きています。
準備フェーズにおいて、V8エンジンはちゃんと `modernGreeting` をEnvironment Recordに登録しています。しかし、`var` と違って初期値(`undefined`など)を自動で代入してくれません。
この「登録はされたけれど、値がまだ割り当てられていない状態のメモリ空間」のことを、専門用語で TDZ(Temporal Dead Zone:一時的死活領域) と呼びます。
このTDZの中にいる変数にアクセスしようとすると、V8エンジンは安全のために容赦なく `ReferenceError` を投げ捨てるのです。これが、`let` や `const` が安全だと言われる理由です。
—
4. 陥りやすい罠:巻き上げによるバグを防ぐために
これらの仕組みを知らないと、現場で思わぬバグに直面します。例えば、次のようなコードです。
// ありがちなアンチパターン
var count = 10;
function processItems() {
// ここでうっかり外側の count を使いたいはずが…
console.log(count); // あれ? undefined になる!?
// 何らかの処理のつもりで、関数内で同じ名前の変数を作ってしまった
var count = 20;
console.log(count); // 20
}
processItems();
1つ目の `console.log(count)` で、関数内の下の方にある `var count = 20;` の巻き上げ(宣言の準備)が影響し、外側のグローバルな `10` ではなく、関数内の `undefined` が参照されてしまうという有名なバグです。
綺麗にコードを書くためのベストプラクティス
1. `var` は使わない: モダンな開発では、基本的に `var` の使用は避け、`let` と `const` を使いましょう。TDZのおかげで、意図しない変数の使い回しやバグを早期に発見できます。
2. 変数は「使う場所の直前」で宣言する: スコープの先頭でまとめて変数を宣言する古い書き方(ホーイスティングを前提とした書き方)はもう必要ありません。コードの可読性を高めるためにも、変数は使う直前で宣言する習慣をつけましょう。
—
まとめ
いかがでしたでしょうか? 今回のポイントをギュッと凝縮して振り返ってみましょう。
- 「巻き上げ」とは、コードが物理的に上に移動する現象ではなく、V8エンジンが実行前にEnvironment Recordへ変数を登録する「準備フェーズ」の現れである。
- `var` は宣言と同時に `undefined` で初期化されるため、宣言前でもアクセスできてしまう(これが混乱の元)。
- `let` や `const` も巻き上げは起きているが、初期化されないTDZ(一時的死活領域)が存在するため、安全にエラーを出して守ってくれる。
この裏側のメカニズム(フェーズの分離とメモリの割り当て)を頭の中にイメージできるようになれば、JavaScriptのスコープで迷うことはもうありません。
ここをクリアしたあなたなら、どんな複雑な非同期処理やクロージャに出会っても、裏側でV8エンジンがどう動いているのかを脳内トレースできるはずです。自信を持って、次のステップへ進んでいきましょう!