【入門編】【上級者向け】JavaScriptの実行コンテキストを自作する:インタプリタの仕組みから学ぶスコープチェーンの正体 – JavaScriptコア文法・モダン言語仕様と変数・関数解析バイブル

こんにちは!普段何気なく書いているJavaScriptですが、「変数ってメモリ上でどう管理されているんだろう?」「スコープチェーンって名前の通り、実際どうやって鎖のように繋がっているんだろう?」と気になったことはありませんか?

他の言語からやってきた開発者や、これからフロントエンド・Node.jsの深部を極めようとする方にとって、この「スコープ」と「変数管理」の仕組みは、最初の大きな壁であり、同時にJavaScriptを完全に掌握するための最重要ポイントです。

今回は、ネットにあるありふれた解説記事は一切ナシで、V8などのJavaScriptエンジンの頭脳をギュッと凝縮した「簡易JavaScriptインタプリタ」を一緒に作りながら、スコープチェーンと実行コンテキストの正体を丸裸にしていきましょう!

ここをクリアすれば、JavaScriptのランタイムの動きがあなたの脳内で完全に可視化されますよ。それでは、熱いコードの旅に出発しましょう!

—

1. なぜ「実行コンテキスト」と「スコープチェーン」を知る必要があるのか?

私たちが普段書くJavaScriptのコードは、そのままブラウザやNode.jsで動いているわけではありません。V8などのエンジンがコードをパースし、「実行コンテキスト(Execution Context)」という目に見えない実行環境の箱を作って、その中で処理を進めています。

よく「変数の巻き上げ(Hoisting)が起きて……」とか「クロージャが変数を保持して……」と言いますが、これらはすべて「Environment Record(環境レコード)」というデータの塊と、それを繋ぐ「スコープチェーン(Outer Lexical Environment Reference)」の仕組みそのものです。

言葉で聞くと難しく聞こえますが、要するにJavaScriptのエンジンは「変数の名前を探すとき、自分の部屋(内側のスコープ)になければ、隣の部屋(外側のスコープ)を順に覗きに行っている」だけなんです。この探索の仕組みを、自前のコードで再現してみましょう!

—

2. 脳内イメージ:スコープチェーンは「マトリョーシカ構造」の辞書である

コードを書き始める前に、イメージを共有しておきますね。
スコープチェーンとは、いわば「名前の引き継ぎができる辞書の束(マトリョーシカ)」です。

1. 一番内側のスコープ(関数の中など)で変数が見つからなければ、
2. 一つ外側のスコープ(親の関数やグローバル)の辞書をめくり、
3. それでもなければさらに外側へ……と、チェーンをたどって探します。

この構造を、そのままJavaScriptのオブジェクトとクラスで表現してみましょう。

—

3. 実践:純粋なJSで「簡易インタプリタ(Environment & Scope)」を実装する

それでは、JavaScriptのコア文法を極限まで理解するために、Environment(環境)クラスを実装してみましょう。以下のコードは、まさにV8エンジンのミニチュア版です。

/

  • 【環境レコード(Environment Record)クラス】
  • 変数の保管場所(辞書)と、外側へのリンク(スコープチェーン)を管理します。

/
class Environment {
constructor(outer = null) {
// このスコープが持つ変数のストレージ(ハッシュマップ)
this.variableMap = new Map();
// 外側の環境(スコープチェーンの鎖)への参照
this.outer = outer;
}

/

  • 現在のスコープに変数を定義する(letやconstのイメージ)

/
define(name, value) {
if (this.variableMap.has(name)) {
throw new Error(`SyntaxError: 変数 ‘${name}’ は既にこのスコープで宣言されています。`);
}
this.variableMap.set(name, value);
console.log(`[環境記録] 変数定義: ${name} = ${value}`);
}

/

  • 変数の値を代入する(すでに存在していなければ外側を探す)

/
assign(name, value) {
if (this.variableMap.has(name)) {
this.variableMap.set(name, value);
console.log(`[環境記録] 変数更新: ${name} = ${value}`);
return;
}

// 現在のスコープになければ、外側のスコープチェーンを辿る(LHSルックアップ)
if (this.outer !== null) {
this.outer.assign(name, value);
return;
}

throw new Error(`ReferenceError: ${name} が定義されていません(代入失敗)。`);
}

/

  • 変数を検索して値を取り出す(RHSルックアップ)

/
get(name) {
// 1. まず自分の部屋(現在の環境)を探す
if (this.variableMap.has(name)) {
return this.variableMap.get(name);
}

// 2. なければ、外側のスコープチェーンへバトンを渡す
if (this.outer !== null) {
console.log(`[スコープチェーン] ‘${name}’ が見つからないため、外側のスコープを検索します…`);
return this.outer.get(name);
}

// 3. グローバルまでたどり着いてもなければエラー
throw new Error(`ReferenceError: ${name} は定義されていません。`);
}
}

// ==========================================
// さあ、実際にこの「簡易インタプリタ」を動かしてみましょう!
// ==========================================

console.log(“— 1. グローバル環境の構築 —“);
const globalEnv = new Environment(null); // 一番外側なので outer は null
globalEnv.define(“appName”, “JS Interpreter v1.0”);
globalEnv.define(“version”, 1);

console.log(“\n— 2. 関数スコープ(ローカル環境)の作成とネスト —“);
// グローバルを外側(outer)に持つ、新しいローカル環境を作る(関数の実行コンテキストのイメージ)
const functionEnv = new Environment(globalEnv);
functionEnv.define(“version”, 2); // シャドーイング(外側と同じ名前の変数)
functionEnv.define(“localMessage”, “こんにちは、スコープの世界へ!”);

// 検索のテスト(RHSルックアップ)
console.log(“\n— 3. スコープチェーンを通じた変数探索のテスト —“);
console.log(“localMessage の取得:”, functionEnv.get(“localMessage”)); // ローカルで見つかる
console.log(“version の取得:”, functionEnv.get(“version”)); // ローカルの version=2 が優先される(シャドーイング)
console.log(“appName の取得:”, functionEnv.get(“appName”)); // ローカルにないので、外側のグローバルから見つかる!

// エラーハンドリングのテスト
try {
console.log(functionEnv.get(“notExistVar”));
} catch (e) {
console.error(“キャッチしたエラー:”, e.message);
}

このコードが教えてくれる本質

上のコードを実行すると、コンソールには次のようなログが流れます。

1. `localMessage` は `functionEnv`(内側)のマップに即座に見つかります。
2. `appName` は `functionEnv` には存在しませんが、`outer` プロパティをたどって `globalEnv` から見つけ出されます。これがまさに「スコープチェーンの探索アルゴリズム」そのものです。
3. `version` は内側にも外側にも存在しますが、内側(ローカル)が優先されていますよね。これがプログラミングにおける「変数のシャドーイング(遮蔽)」のメカニズムです。

—

4. 陥りやすい罠:なぜクロージャはメモリに残り続けるのか?

このインタプリタの仕組みを理解すると、JavaScriptでよくある「クロージャ(Closure)」の挙動も完全に腑に落ちます。

通常の関数であれば、関数が実行終わればそのローカル環境(上のコードでいう `functionEnv`)はガベージコレクタに回収され、消え去るはずです。
しかし、関数が内部で別の関数を返却し、その返却された関数が外側の環境の変数を参照し続けている場合、外側の環境への参照(outer)が生き残り続けます。

これが、V8エンジンのヒープメモリ上で「環境レコードが解放されずに残り続ける」という現象の正体です。メモリリークを避けるために不要になったクロージャを解放すべきと言われるのは、この参照の鎖を切る必要があるからなんですね。

—

まとめ:ここをクリアすれば、JavaScriptの基本はバッチリマスターできますよ!

今回は、変数のスコープやチェーンの裏側にある「Environment Record」の概念を、自作の簡易インタプリタを通じて紐解いてみました。

  • 変数の検索は、内側の環境から外側の環境へ(スコープチェーン)順番に行われる。
  • 同名の変数が存在する場合、より内側のスコープの変数が優先される(シャドーイング)。
  • 外側の環境への参照が維持されることが、クロージャの正体である。

このメカニズムが頭に入っていれば、今後どれほど複雑な非同期処理や、スコープが入り組んだモダンなフレームワークのコードに出会っても、「今、どの環境レコードのどこを見ているのか?」を正確に脳内トレースできるようになります。

JavaScriptの足回りを支えるこの美しい仕組みを味方につけて、さらに深みのあるエンジニアリングを楽しんでいきましょう!

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