こんにちは!フロントエンドからNode.jsの内部構造まで、日夜JavaScriptと向き合っているシニアアーキテクトです。
他のプログラミング言語からJavaScriptの世界へやってくると、時々不思議な現象に出会いませんか?「あれ、変数を宣言する前になぜかアクセスできちゃうぞ?(でも `undefined` になる)」とか、「関数の中で作ったはずの変数が、外から見えなくなっているのはなぜだろう?」といった疑問です。
実は、JavaScriptのエンジン(V8など)の頭の中を覗いてみると、そこには「実行コンテキスト」と「スコープチェーン」という美しくも厳格なルールが存在しています。
今回は、このJavaScriptの心臓部とも言える「名前解決の仕組み」を完全に自分のものにするために、JavaScriptを使って「簡易インタプリタ(実行環境)」を自作してみましょう!ここをクリアすれば、JavaScriptの変数スコープの挙動で迷うことはもう二度となくなりますよ。バッチリマスターしていきましょう!
—
1. そもそも「スコープチェーン」の正体って何だろう?
JavaScriptでコードを実行するとき、エンジンは「今、どの変数にアクセスできるか」を管理するためのメモ帳のような空間を用意します。これが実行コンテキスト(Execution Context)です。
そして、関数が入れ子になっているとき、内側の関数から外側の関数の変数へアクセスできるように、メモ帳同士を鎖(チェーン)のように繋げていく仕組みをスコープチェーンと呼びます。
イメージ図にしてみると、こんな感じです:
[ グローバル実行コンテキスト (Global Environment) ]
└─ 変数: globalVar = “地球”
│
▼ (スコープチェーンで連結)
[ 外側関数の実行コンテキスト (Outer Environment) ]
└─ 変数: outerVar = “大陸”
│
▼ (スコープチェーンで連結)
[ 内側関数の実行コンテキスト (Inner Environment) ]
└─ 変数: innerVar = “部屋”
内側の世界で `innerVar` が見つからなければ外側(大陸)を探し、そこにもなければさらに外(地球)を探す……これが名前解決の旅の正体です。
—
2. 【ハンズオン】JavaScriptでスコープチェーンを自作してみよう!
百聞は一見にしかず。この仕組みを、私たちの手でコードとして再現してみましょう。
以下のコードは、JavaScriptの変数環境(Environment Record)とスコープチェーンを模した、小さなインタプリタの仕組みです。
/
- 実行環境(スコープ)を表現するクラス
/
class Environment {
// 親環境(外側のスコープ)への参照と、このスコープが持つ変数の箱を用意します
constructor(parent = null) {
this.variables = {}; // このスコープで宣言された変数を保持する
this.parent = parent; // 外側のスコープ(親)へのリンク(これがスコープチェーンの正体!)
}
// 変数をこのスコープに定義する(letやconstのイメージ)
define(name, value) {
this.variables[name] = value;
}
// 変数を探す(名前解決の旅)
get(name) {
// 1. まず自分のスコープの箱を確認する
if (Object.prototype.hasOwnProperty.call(this.variables, name)) {
return this.variables[name];
}
// 2. 見つからなかったら、親のスコープへバトンタッチ(再帰的に探索)
if (this.parent !== null) {
return this.parent.get(name);
}
// 3. 根っこの親(グローバル)まで探してもなければエラー!
throw new ReferenceError(`変数 ‘${name}’ が定義されていません(ReferenceError)`);
}
}
// ==========================================
// さあ、私たちが作った自作エンジンを動かしてみましょう!
// ==========================================
// 1. グローバル環境を作る
const globalEnv = new Environment();
globalEnv.define(‘appName’, ‘自作JSインタプリタ’);
globalEnv.define(‘version’, 1.0);
// 2. 関数が呼び出されたときのイメージ:グローバルを親に持つ「ローカル環境」を作る
const functionEnv = new Environment(globalEnv);
functionEnv.define(‘userName’, ‘Alice’);
// — テスト実行 —
console.log(functionEnv.get(‘userName’)); // 自分のスコープにあるので “Alice” が返る
console.log(functionEnv.get(‘appName’)); // 自分には無いので、親(グローバル)まで探しに行って “自作JSインタプリタ” を取得!
try {
console.log(functionEnv.get(‘secretKey’)); // どこにも無いのでエラーになる
} catch (e) {
console.error(e.message); // 出力: 変数 ‘secretKey’ が定義されていません(ReferenceError)
}
このコードの `parent` プロパティの存在こそが、スコープチェーンそのものです。内側の環境から外側の環境へとポインタが一本の鎖で繋がっているのが、直感的に分かりますよね。
—
3. なぜ「ReferenceError」が起きるのか?
私たちが普段書いているJavaScriptでも、存在しない変数にアクセスしようとすると `ReferenceError` が発生します。
これは、V8エンジンがまさに先ほど私たちが書いた `get()` メソッドのように、現在のスコープからグローバルスコープのてっぺん(グローバルオブジェクトのさらに外側)まで探し回った結果、「どこにもそんな名前の変数はありませんでした」とギブアップした瞬間に発生するエラーなのです。
逆に、変数は見つかったけれど値がまだ代入されていない場合は `undefined` になります。これも環境の箱の中に「キー(名前)」だけが先登録されている状態(巻き上げ / Hoisting の一幕)を思い浮かべれば、完全に合点がいきますね。
—
4. 陥りやすい罠:レキシカルスコープ(静的スコープ)の重要性
ここで一つ、知的なプログラミングの罠についてお話しておきます。
スコープチェーンは、「関数をどこで呼んだか」ではなく、「関数をどこで書いたか(定義したか)」によって決定されます。これをレキシカルスコープ(静的スコープ)と呼びます。
先ほどの自作インタプリタで言えば、関数オブジェクトが作られた瞬間に、その時の `Environment`(親の顔)がガッチリと固定(記憶)されます。これが、いわゆるクロージャ(Closure)を生み出す母体でもあります。
もし「関数を呼んだ場所」でスコープが決まってしまうと(動的スコープ)、プログラムの挙動があちこちで変わってしまい、コードの推論が極めて困難になります。JavaScriptの設計者たちは、私たちの脳の負担を減らすために、書いた場所でスコープが確定する美しい静的構造を採用してくれたのです。
—
まとめ
いかがでしたでしょうか?
普段何気なく使っている `let` や `const`、そして関数やスコープの裏側では、今回私たちが自作したような「環境のツリー構造と名前解決の旅」がコンマ数秒の間に何千回、何万回と行われています。
- スコープチェーンとは、外側の環境への参照(リンク)の鎖である。
- 変数の名前解決とは、内側から外側へ向かって変数の箱を探す旅である。
- 見つからなければ `ReferenceError`、名前はあるが中身がなければ `undefined` になる。
このメカニズムを脳内にハッキリとイメージできるようになれば、複雑な非同期処理やクロージャを書く時も、迷うことなくコードの挙動を完全にコントロールできるようになりますよ。
今日の学びを武器に、ぜひ明日からのコーディングをさらに楽しんでくださいね!