【実務・中級編】 レキシカル環境と環境レコードの内部構造 – JavaScript実践ガイド

やあ。現場でコードを書いていて、「なぜこの変数はここで参照できるのか?」「なぜ巻き上げ(Hoisting)という挙動が起きるのか?」とふと立ち止まったことはないかな。

中級者へのステップアップを目指す君なら、`var`, `let`, `const` の違いを「再代入できるかどうか」だけで理解している段階はもう卒業しているはずだ。今日は、JavaScriptエンジンが内部で一体何をやっているのか、その「心臓部」であるレキシカル環境(Lexical Environment)の話をしよう。

ここを理解すると、クロージャの挙動や、TDZ(Temporal Dead Zone)の正体が手に取るようにわかるようになる。コードの裏側が見えるようになれば、バグに遭遇した時のデバッグ速度が劇的に変わるはずだ。

—

1. JavaScriptの「メモリ管理」という名の司令塔

JavaScriptがコードを評価する時、エンジンは「今どの変数がどこで使えるか」を管理するためのデータ構造を作っている。それがレキシカル環境(Lexical Environment)だ。

これは大きく分けて2つの要素で構成されている。

1. 環境レコード(Environment Record): 変数や関数の宣言を実際に記録する「箱」。
2. 外部レキシカル環境への参照(Outer Lexical Environment Reference): 外側のスコープへ繋がる「ポインタ」。

JavaScriptが変数を探すとき、まず現在の環境レコードを見に行き、なければ「外部への参照」を辿って親の環境へ……という旅をする。これが「スコープチェーン」の正体だ。

—

2. Declarative Environment Record:`let`と`const`の厳格さ

`var`は「オブジェクト環境レコード」という緩い仕組みで管理されるが、`let`や`const`は「宣言的環境レコード(Declarative Environment Record)」という、より厳格な仕組みで管理される。

ここで重要なのがTDZ(一時的デッドゾーン)だ。`let`や`const`は、コード実行時に宣言行に到達するまで、環境レコード内に「未初期化状態」として登録される。この「宣言はされているが値が入っていない状態」でアクセスしようとすると、エンジンが「それはダメだ」と例外を投げる。これが `ReferenceError` の真実だ。

実践:スコープの深淵を覗くコード

まずは以下のコードを見てほしい。エンジンが頭の中で何をしているかをコメントに落とし込んだ。

// グローバル環境レコードに ‘outerVar’ が登録される
const outerVar = “私はグローバル”;

function scopeTest() {
// ここで新しい環境レコードが作成される
// ‘innerVar’ はまだ未初期化状態 (TDZの中)

// console.log(innerVar); // ここで呼ぶと ReferenceError

let innerVar = “私はローカル”;

// 初期化完了。これ以降は環境レコードから値が取得できる
console.log(innerVar);

if (true) {
// ブロックが生成されるたびに、新しい環境レコードが積み重なる
const blockVar = “ブロック限定”;
console.log(blockVar);
}

// ここでは blockVar は参照できない(外側の環境レコードに存在しないため)
}

scopeTest();

—

3. なぜ「巻き上げ」という現象が起きるのか?

「巻き上げ(Hoisting)」という言葉のせいで、コードの記述位置が物理的に上へ移動しているような誤解をする人が多い。だが、実際は違う。

JavaScriptエンジンはコードを実行する前にコンパイルフェーズ(解析フェーズ)という準備運動を行う。この時、`var`や`function`の宣言を環境レコードに先に登録してしまうんだ。

  • `var`: 登録時に `undefined` で初期化される。だから参照してもエラーにならない。
  • `let/const`: 登録はされるが、初期化はされない(TDZ)。だからエラーになる。

この「事前の登録」という行為があるおかげで、我々は関数が定義される前にその関数を呼び出したりできる。これはJavaScriptという言語の柔軟性であり、同時に注意深く扱うべき設計でもある。

—

4. プロの現場での運用Tips

この知識をどう実務に活かすか? 私がチームメンバーによく伝えるのは以下の3点だ。

1. `var`は禁止: `var`はスコープが関数単位で広く、再宣言も許容する。現代のJavaScriptでは百害あって一利なしだ。迷わず `const` を使い、再代入が必要な時だけ `let` にする。
2. 変数は使う直前で宣言する: 昔のJS(ES5以前)では、関数の冒頭で変数をまとめて宣言する慣習があったが、今は違う。TDZを安全に利用するためにも、変数は使う直前のスコープで宣言し、生存期間を極限まで短くする。
3. クロージャを恐れない: クロージャは「内部的な環境レコードへの参照を保持し続ける」という仕組みだ。これを知っていれば、メモリリークのリスクを意識しながら、強力なカプセル化(モジュールパターン)を自在に操れるようになる。

—

最後に

JavaScriptの仕様は、一見すると直感的でない挙動に見えるかもしれない。しかし、その裏側にある「環境レコード」や「スコープチェーン」の仕組みを理解してしまえば、それは「挙動」ではなく「設計」として腑に落ちるはずだ。

「なぜ動くか」を説明できるエンジニアは、たとえどんな難解なバグにぶち当たっても、論理的な推論で必ず答えにたどり着くことができる。君もぜひ、ブラウザのデバッガーで「Scope」パネルを覗き込み、変数たちがどう管理されているのか、自分の目で確認してみてほしい。

また何か詰まったら、いつでも聞きに来るといい。現場からは以上だ。

コメント

タイトルとURLをコピーしました