【入門編】JavaScriptのスコープ階層を可視化する:Lexical EnvironmentとEnvironment Recordの構造 – JavaScriptコア文法・モダン言語仕様と変数・関数解析バイブル

こんにちは!フロントエンドからNode.jsの内部まで、日々JavaScriptのエンジンと対話しているフルスタックエンジニアの先輩です。

今回は、JavaScriptを学び始めた誰もが一度はつまずく、そして中級者へのステップアップに不可欠な「スコープ階層と変数の検索メカニズム」についてお話しします。

「なぜ変数が勝手に上の階層から見つかるの?」「`var`と`let`で何が違うの?」といった疑問を、ECMAScript仕様書の裏側にあるLexical Environment(レキシカル環境)とEnvironment Record(環境レコード)という概念を使って、分かりやすく解き明かしていきましょう。

ここをクリアすれば、JavaScriptの変数管理の仕組みはバッチリマスターできますよ。それでは、エンジニアの脳内を覗く旅に出発しましょう!

—

1. 変数はどこに保存され、どうやって見つかるのか?

JavaScriptのコードを書くとき、私たちは何気なく `let` や `const` で変数を宣言し、それを関数の中で使っていますよね。

const globalMessage = “こんにちは”;

function sayHello() {
const localMessage = “世界”;
console.log(globalMessage + “、” + localMessage); // どっちの変数も使える!
}

sayHello();

このコード、動かすのは簡単ですが、「JavaScriptエンジンはこの時、裏で何をしているのか?」を考えたことはありますか?

実は、JavaScriptエンジン(V8など)は、変数を管理するために「Lexical Environment(レキシカル環境)」という目に見えない箱(データ構造)を作っています。そして、変数を探すときには、この箱を外側に向かって順番に覗き込んでいくのです。

この仕組みをイメージ図で表してみましょう。

[ グローバル・レキシカル環境 ]
┣ globalMessage: “こんにちは”
┗ 外側への参照: null (一番外側)
↑
│ (見つからない場合は外側を辿る)
│
[ sayHello関数のレキシカル環境 ]
┣ localMessage: “世界”
┗ 外側への参照: グローバル・レキシカル環境を指している

この「外側への参照」のことを、仕様書では Outer Lexical Environment Reference と呼びます。この参照が鎖(チェーン)のように繋がっているため、私たちはこれを「スコープチェーン」と呼んでいるんです。

—

2. 仕様書の裏側:Lexical Environment と Environment Record

もう少しだけ、JavaScriptの心臓部であるECMAScript仕様書の世界を覗いてみましょう。

レキシカル環境は、主に以下の2つの要素で構成されています。

1. Environment Record(環境レコード): そのスコープ内で宣言された変数や関数の実データを記録する場所。
2. Outer Lexical Environment Reference(外側レキシカル環境への参照): 親となるスコープへのポインタ。

変数にアクセスしようとしたとき、JavaScriptエンジンは以下のアルゴリズム(手順)で変数を探し出します。

1. 現在のEnvironment Recordを確認する。そこに変数があれば、それを返す。
2. なければ、Outer Lexical Environment Referenceが指す親のレキシカル環境へ移動する。
3. そこでも見つからなければ、さらに親へ……と連鎖を辿っていく。
4. グローバル環境の親(`null`)に到達しても見つからない場合、`ReferenceError`(参照エラー)をスローする。

言葉だけだと難しく感じるかもしれませんが、要するに「手元になければ、親御さんに聞きに行く。親も知らなかったら、その親に……という伝言ゲーム方式」ですね。

—

3. 実コードでスコープチェーンの動きを脳内トレースする

では、実際にコードを書いて、この検索アルゴリズムがどう動くのかをデバッグの視点で確認してみましょう。

// レベル1: グローバルスコープ
const tier = “S”;

function outerFunction() {
// レベル2: outerFunctionのスコープ
const rank = “A”;

function innerFunction() {
// レベル3: innerFunctionのスコープ
const score = 100;

// 変数「tier」「rank」「score」をすべて参照してみる
console.log(`スコア: ${score}, ランク: ${rank}, ティア: ${tier}`);
}

innerFunction();
}

outerFunction();
// 実行結果: スコア: 100, ランク: A, ティア: S

なぜすべての変数が見つかるのか?

`innerFunction` の中で `tier` を使おうとしたとき、以下の検索が行われます。

1. `innerFunction` の Environment Record を見る → `score` はあるが `tier` はない。
2. 外側への参照を辿って、`outerFunction` の Environment Record を見る → `rank` はあるが `tier` はない。
3. さらに外側への参照を辿って、グローバル環境の Environment Record を見る → `tier` があった! 取得完了。

このように、内側のスコープから外側のスコープへ向かって、一方向にのみ変数を探しに行くのがJavaScriptの基本ルールです。逆に、外側のスコープから内側のスコープ(例:`outerFunction` から `innerFunction` の `score`)を見ることは絶対にできません。

—

4. 陥りやすい罠:スコープの勘違いとエラー

初学者のうちによくやってしまう、スコープにまつわるエラーやバグの例を見ておきましょう。これを知っておくだけで、現場でのデバッグスピードが劇的に上がります。

罠1: ブロックスコープ(`let` / `const`)の壁

`var` は関数スコープですが、現代の主流である `let` と `const` はブロック(`{}`)スコープを持ちます。つまり、`if`文や `for`文のブロックも、それぞれ独自のレキシカル環境を作ります。

function checkAccess(age) {
if (age >= 18) {
// if文のブロック内に作られたレキシカル環境
let message = “成人です”;
console.log(message); // ここでは読める
}

// 👇 ここでエラーになる!
// messageはif文のブロックの中だけの秘密だから、外からは見えない
console.log(message);
}

checkAccess(20);
// 実行結果: ReferenceError: message is not defined

先輩からのアドバイス:
「変数は、それを必要とする最も狭いブロック(あるいはスコープ)で宣言する」というのがモダンなJavaScriptの鉄則です。無駄に広いスコープで変数を定義すると、メモリの無駄遣い(V8のガベージコレクションが回収しにくくなる原因)や、予期せぬ変数の上書きバグに繋がります。

—

まとめ

いかがでしたでしょうか? 今回の重要ポイントをサクッと振り返ってみましょう。

  • JavaScriptの変数は、Lexical Environment(レキシカル環境)という箱の中で管理されている。
  • 箱の中には、変数の実データを記録する Environment Record と、親を指す 外側への参照 がある。
  • 変数が見つからない場合、エンジンはこの参照を辿って外側へ外側へと検索していく(これがスコープチェーン)。
  • `let` や `const` はブロック単位で新しい環境を作るため、スコープの範囲に注意が必要。

この「レキシカル環境とスコープチェーン」の仕組みが頭の中にイメージできるようになると、コードを書くときに「今、自分の書いている変数はどこから見えて、どこから見えないのか?」が手に取るように分かるようになります。

ここをクリアしたあなたなら、クロージャ(Closure)や非同期処理の挙動もスムーズに理解できるはずです。自信を持って次のステップへ進んでくださいね!

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