こんにちは!日々の開発、本当にお疲れ様です。
JavaScriptを書いていて、「あれ、なんでこの変数、宣言する前なのに参照エラーにならないんだろう?」とか、「`let`に変えた途端に『初期化前にアクセスできません』って怒られたぞ…?」と首を傾げた経験はありませんか?
ネットの入門書には「`var`は巻き上げが起きるよ」「`let`はブロックScopedだよ」となんとなく書かれていますが、実はこれ、JavaScriptのエンジン(Google ChromeやNode.jsで使われている V8 など)が、裏側のメモリ上で「いつ、どこで、どうやって変数の席を用意しているか」を知るだけで、すべてが綺麗に氷解するんです。
ここをクリアすれば、JavaScriptのランタイムの挙動が手に取るようにわかるようになりますよ。さあ、V8エンジンの脳内へ一緒に潜ってみましょう!
—
1. そもそも「変数の宣言」って、メモリ上で何が起きているの?
JavaScriptのコードが実行されるとき、V8エンジンはただ上から順に文字を読んでいるわけではありません。コードを実際に動かす前に、必ず「作成フェーズ(Creation Phase)」という事前準備の時間を取ります。
この準備フェーズで、V8はコード内にある変数や関数の名前をあらかじめスキャンし、メモリ上に「Environment Record(環境レコード)」という名前の台帳(ようなもの)を作ります。
この台帳のどこに、どうやって名前が登録されるかによって、`var` と `let`(および `const`)の運命が決定的に分かれるんです。
—
2. varの正体:巻き上げ(Hoisting)と「undefined」の甘い罠
まずは、古き良き `var` から見ていきましょう。
`var` で宣言した変数は、コードのどこに書いてあっても、スコープの先頭に「巻き上げられる」というのは有名ですね。
でも、正確には「コードが上に移動している」わけではありません。V8が作成フェーズの段階で、Environment Recordに変数の名前を登録し、同時に「初期値として `undefined` を強制的にブチ込んでいる」というのが真実です。
以下のコードを見てください。
// まだ名前を宣言していないつもり…?
console.log(myFavoriteFramework); // 出力結果: undefined (エラーにならない!)
var myFavoriteFramework = “V8 Engine”;
console.log(myFavoriteFramework); // 出力結果: “V8 Engine”
メモリの動きを脳内トレースしてみよう
1. 作成フェーズ:
V8はコード全体をスキャンし、Environment Recordに `myFavoriteFramework` という名前の箱を作ります。そして、優しく(あるいは大きなお世話として)自動的に `undefined` という値をその箱に詰めておきます。
2. 実行フェーズ(1行目 `console.log`):
「`myFavoriteFramework` をくれ!」と言われたV8は、台帳を見に行きます。すると、すでに箱があり、中身も `undefined` として準備されているため、エラーを出さずにそのまま `undefined` を返します。これが「巻き上げ」の正体です。
初心者キラーとして有名な挙動ですが、V8のメモリ構造を知れば「そりゃ最初から `undefined` が入ってるんだからエラーにならないよね」と納得できますよね。
—
3. let / constの正体:TDZ(一時的死域)の厳格な守り
では、モダンなJavaScriptの主役である `let` や `const` はどうでしょうか?
「じゃあ `let` も同じように巻き上げられて、最初は `undefined` が入るのかな?」と思って試してみると、痛い目を見ます。
// letで宣言する前にアクセスしてみる
console.log(secretWeapon);
// 💥 致命的エラー: Cannot access ‘secretWeapon’ before initialization
// (初期化する前にアクセスすることはできません)
let secretWeapon = “JIT Compiler”;
このエラー、怖くて潔いですよね。「おいおい、まだ定義してないのに触るな!」とV8がビシッと止めてくれます。このエラーが出るエリアのことを、エンジニアの世界で TDZ(Temporal Dead Zone:一時的死域) と呼びます。
なぜTDZが存在するのか?メモリの挙動で解説
`let` や `const` の場合、V8の作成フェーズにおける振る舞いが `var` とは根本的に異なります。
1. 作成フェーズ:
V8はEnvironment Recordに `secretWeapon` という名前の「箱」の存在こそ登録します。しかし、この時点ではまだメモリ上の領域(初期値)を割り当てません(未初期化状態)。
2. TDZ(一時的死域)の発生:
箱はあるのに、中身が空っぽどころか「まだ使っちゃいけないロック」がかかっている状態、これがTDZです。この期間に変数に触ろうとすると、V8は安全のために即座にエラーをスローします。
3. 実行フェーズ(コードがその行に到達した瞬間):
`let secretWeapon = “JIT Compiler”;` の行に実行が到達した瞬間、初めてメモリが割り当てられ、値が代入され、ロックが解除されます。
—
4. 【決定的な違いのまとめ】Environment Recordの構造比較
頭の中を整理するために、作成フェーズにおける `var` と `let` のメモリ構造の違いを対比させてみましょう。
| 項目 | `var` の場合 | `let` / `const` の場合 |
| :— | :— | :— |
| 巻き上げ(Hoisting) | あり | あり(※ただし挙動が異なる) |
| 作成フェーズでの初期化 | 自動的に `undefined` を代入 | 初期化しない(未初期化のまま) |
| TDZ(一時的死域) | なし(エラーにならない) | あり(アクセスするとエラー) |
| バグの発見難易度 | 気づきにくい(サイレントバグの温床) | 即座にエラーで気づける(堅牢) |
`var` は「とりあえず箱を作って、中身は空っぽ(undefined)にしておくね!」というお節介な仕様でした。
一方、`let` や `const` は「コードが実際にその行を通るまで、絶対に触らせない!バグを防ぐためだ!」という、V8からの非常に厳格で愛のあるセキュリティ対策なんです。
—
5. 実務で役立つ!スコープチェーンとメモリの開放
最後に、変数が役目を終えたあとのメモリの運命についても少しだけ触れておきますね。
JavaScriptでは、関数が実行されるたびに新しい実行コンテキストが作られ、そこに紐づくEnvironment Recordが生成されます。
関数が終了すると、そのEnvironment Recordが指していたメモリ領域は、V8の強力なガベージコレクタ(GC)によって綺麗に掃除(解放)されます。
ただし、クロージャ(Closure)を使っている場合は注意が必要です。内側の関数が外側の変数を参照し続けていると、V8は「まだこの変数は使われるかもしれない!」と判断し、Environment Recordのメモリをヒープ領域に残し続けます。
function createCounter() {
let count = 0; // このcountの入ったEnvironment Recordは保持され続ける
return function() {
count++;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
このように、変数の宣言(`var` / `let` / `const`)をどこで行うか、そしてどう振る舞うかを知ることは、単なる文法暗記ではなく、V8エンジンのメモリ消費量をコントロールし、メモリリークを防ぐための第一歩なのです。
—
おわりに
いかがでしたでしょうか?
「なぜエラーになるのか」「なぜ動くのか」の裏側にあるV8エンジンのメモリレイアウトとEnvironment Recordの仕組みを知ることで、JavaScriptという言語の輪郭がグッと鮮明に見えてきたはずです。
ここをクリアできれば、もう変数の挙動で迷子になることはありません。自信を持って、よりモダンで堅牢なコードを書いていきましょう!
それでは、次のステップでも一緒に楽しくJavaScriptを極めていきましょうね!