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

こんにちは!フロントエンドからNode.jsの深部まで、JavaScriptのエンジンが裏側でどう動いているのかを日々追いかけているシニアアーキテクトです。

今回は、JavaScriptを学ぶ誰もが一度はつまずく、そして中級者への大きな壁となる「スコープチェーンと変数検索のメカニズム」について、お話ししていきます。

ネット検索をすれば「`let`と`var`の違いは〜」「スコープとは変数の有効範囲です」といったお決まりの説明がたくさん出てきますよね。でも、それだけでは「なぜネストした関数から外側の変数が見えるのか」「変数が増えると検索コストはどうなるのか」という本質的な疑問の答えにはなりません。

今回は、あえてJavaScriptのインタプリタ(エンジン)がやっていることを自分の手で模倣した「ミニ・スコープ管理オブジェクト」を作りながら、スコープチェーンの正体を丸裸にしていきましょう。ここをクリアすれば、あなたのJavaScriptのコードを見る目は劇的に変わりますよ。

—

1. スコープチェーンって、そもそも一体なんだろう?

JavaScriptでコードを書くとき、私たちは何気なく「外側の関数で定義した変数」を内側の関数から呼び出していますよね。

const globalMessage = “こんにちは、”;

function outerFunction(name) {
const outerMessage = “V8の世界へ”;

function innerFunction() {
// 外側の変数やグローバル変数に普通にアクセスできる!
console.log(globalMessage + name + “さん、” + outerMessage + “!”);
}

innerFunction();
}

outerFunction(“研修生”); // 実行結果: こんにちは、研修生さん、V8の世界へ!

この「内側の関数から外側、さらにその外側へと変数を探しに行く仕組み」を、私たちはスコープチェーン(Scope Chain)と呼んでいます。

プログラミング初学者のうちは「魔法のようにつながっている」と感じるかもしれませんが、V8のようなJavaScriptエンジンは、魔法を使っているわけではありません。エンジンは、それぞれのスコープを「親子関係でつながったオブジェクトの鎖(チェーン)」として厳密に管理しています。

この鎖の構造を、JavaScriptのコードそのもので再現してみましょう。

—

2. 独自のスコープ管理オブジェクトを実装してみる

JavaScriptの実行エンジンが裏側でやっている変数検索の旅を、自分でコードにして可視化してみます。以下のコードを読んでみてください。

/

  • スコープ(変数環境)を表すクラス

/
class Scope {
// 親スコープへの参照(これがスコープチェーンの「鎖」になります)
constructor(parentScope = null) {
this.variables = {}; // このスコープが保持する変数たち
this.parent = parentScope; // 外側のスコープへのリンク
}

// 変数を現在のスコープに定義する
define(name, value) {
this.variables[name] = value;
}

// 変数を検索する(見つからなければ親スコープを辿る!)
lookup(name) {
// 1. まず自分のスコープ(variables)をチェック
if (Object.prototype.hasOwnProperty.call(this.variables, name)) {
console.log(`[Hit!] スコープ内で変数 “${name}” を発見しました。`);
return this.variables[name];
}

// 2. 自宅になければ、親スコープ(外側)へ探しに行く(再帰的探索)
if (this.parent !== null) {
console.log(`[Miss…] 現在のスコープに “${name}” がないため、外側のスコープへ移動します。`);
return this.parent.lookup(name);
}

// 3. 根元(グローバル)まで探しても見つからなければエラー
throw new ReferenceError(`ReferenceError: “${name}” は定義されていません。`);
}
}

// — 実際にスコープチェーンを構築して動かしてみよう —

// 1. グローバルスコープの作成
const globalScope = new Scope(null);
globalScope.define(“appTitle”, “JS Interpreter v1.0”);

// 2. 関数スコープ(outer)の作成、親にグローバルを指定
const outerScope = new Scope(globalScope);
outerScope.define(“userName”, “Alice”);

// 3. さらに内側の関数スコープ(inner)の作成、親にouterを指定
const innerScope = new Scope(outerScope);
innerScope.define(“localSecret”, “42”);

// — 変数の探索コスト(検索の旅)を体験する —

console.log(“— ケース1: 内側から一番外側(グローバル)の変数を探す —“);
const title = innerScope.lookup(“appTitle”);
console.log(“取得結果:”, title);

console.log(“\n— ケース2: 存在しない変数を探してエラーになるケース —“);
try {
innerScope.lookup(“notExistVar”);
} catch (e) {
console.error(e.message);
}

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

— ケース1: 内側から一番外側(グローバル)の変数を探す —
[Miss…] 現在のスコープに “appTitle” がないため、外側のスコープへ移動します。
[Miss…] 現在のスコープに “appTitle” がないため、外側のスコープへ移動します。
[Hit!] スコープ内で変数 “appTitle” を発見しました。
取得結果: JS Interpreter v1.0

— ケース2: 存在しない変数を探してエラーになるケース —
[Miss…] 現在のスコープに “notExistVar” がないため、外側のスコープへ移動します。
[Miss…] 現在のスコープに “notExistVar” がないため、外側のスコープへ移動します。
ReferenceError: “notExistVar” は定義されていません。

これが、スコープチェーンの正体です。内側のコードから外側に向かって、まるでバケツリレーのように一つずつ親のオブジェクトを辿っていく仕組みになっているのです。

—

3. 変数検索コストの重みを知る

さて、ここで少しエンジニアとしての視点を深めましょう。

先ほどのコードで、`innerScope` から `appTitle`(一番外側の変数)を探すとき、スコープを3回辿る(ルックアップする)必要がありました。

もしこれが、何重にも深くネストされた巨大なアプリケーションコードだったらどうでしょう?

  • スコープの階層が深ければ深いほど、変数がヒットするまでの検索コスト(CPUサイクルとメモリ参照のオーバーヘッド)が増加します。
  • 「じゃあ、すべての変数をグローバル(一番外側)に置けば、すぐ見つかるから速いのでは?」と思われるかもしれませんが、それは大間違いです。グローバル変数はメモリの解放(ガベージコレクション)を妨げ、名前の衝突(バグ)を引き起こす最大の元凶になります。

実際のV8エンジンは、このような線形探索(順番に辿っていく方法)を毎回愚直に行っているわけではありません。エンジンは内部で「レキシカル環境(Lexical Environment)」や「クロージャの最適化」「形状(Hidden Class)」といった高度な最適化を行い、変数の位置を高速に特定できるように工夫しています。

しかし、その「最適化の裏側」にある基本原則が、先ほど私たちが自作したような「スコープの鎖」であることに変わりはありません。

—

4. 陥りやすい罠:変数の巻き上げ(Hoisting)とスコープ

初学者のうちによくあるバグの一つに、スコープと変数の宣言に関する勘違いがあります。

function checkScope() {
console.log(secretCode); // ここはどうなる?(エラー? undefined?)

var secretCode = “1234”;
}

checkScope();

答えは `ReferenceError` ではなく、`undefined` ですよね。
これも、JavaScriptエンジンがコードを実行する前に、「作成フェーズ(Creation Phase)」で現在のスコープオブジェクトに対して `var secretCode` というプロパティの枠だけを先に追加し、初期値を `undefined` で埋める(=巻き上げ / Hoisting)という処理を行っているからに他なりません。

一方で、モダンな `let` や `const` は、スコープに登録はされるものの、初期化される前にアクセスすると一時的デッドゾーン(TDZ: Temporal Dead Zone)というセキュリティゾーンに引っかかり、厳格に `ReferenceError` を投げる仕様になっています。これもスコープ管理の仕組みがより安全に進化したがゆえの挙動です。

—

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

いかがでしたでしょうか?
「スコープチェーン」という言葉を聞くと難しく感じるかもしれませんが、その正体は「親子関係を持ったプレーンな検索オブジェクトの鎖」にすぎません。

1. 変数を使うときは、まず自分の足元(現在のスコープ)を探す。
2. なければ、親のスコープへバケツリレー形式で探しに行く。
3. 根元まで見つからなければ `ReferenceError` を投げる。

この脳内イメージ(メンタルモデル)がしっかりと構築できていれば、複雑な非同期処理、クロージャ、モジュールのスコープ構造に直面したときでも、コードが裏側でどう動いているのかを正確にデバッグできるようになります。

スコープと変数検索の仕組みをマスターしたあなたなら、どんなに複雑なコードベースでも恐れるに足リません。自信を持って次のステップへ進んでいきましょう!

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