みなさん、こんにちは!日頃からJavaScriptやNode.jsを書いていると、「この変数はどこから参照できるんだろう?」「グローバル変数を汚染せずに安全にコードを実行するにはどうすればいいんだろう?」と悩むことはありませんか?
JavaScriptを習得する上で、最も重要でありながら多くの人がつまずきやすいのが「スコープ(変数の有効範囲)」と「実行文脈(コンテキスト)」の仕組みです。
普段、私たちが何気なく書いている `let` や `const`、そして `var`。これらがV8エンジン(JavaScriptを動かすエンジン)の中でどのように評価され、メモリ上に配置されているのか。その「真の姿」を理解すると、バグの発生率が劇的に下がり、コードの挙動を完璧に予測できるようになります。
今回は、Node.jsに標準で備わっている`vm`(Virtual Machine)モジュールという最高の実験ツールを使って、JavaScriptのスコープと変数隔離のメカニズムを楽しく、そして本質から学んでいきましょう!
ここをクリアできれば、JavaScriptのスコープに関する基本と応用はバッチリマスターできますよ!
—
1. JavaScriptの「スコープ」と「コンテキスト」の正体
まずは、基礎となる概念をすっきり整理しておきましょう。
普段、私たちがコードを書くとき、変数の宣言には `var`, `let`, `const` を使いますよね。これらは以下のようなスコープのルールを持っています。
- `var`: 関数スコープ(関数単位で区切られる。関数の外ではグローバルオブジェクトのプロパティになりやすい)
- `let` / `const`: ブロックスコープ(`{ … }` の波括弧の中で区切られる)
では、JavaScriptエンジン(V8)がこれらのコードを実行するとき、裏側では何が起きているのでしょうか?
実は、コードを実行するためには、変数を格納するための「グローバルオブジェクト(環境)」と、そのコードがどの領域で評価されているかを示す「コンテキスト(実行文脈)」という舞台が必要になります。
【V8エンジンの実行イメージ】
[ メインのJavaScript実行環境 ]
├── グローバルオブジェクト (global / window)
└── メインのスコープチェーン (let / const / var の管理領域)
〜〜〜〜〜〜 隔離の壁 〜〜〜〜〜〜
[ vm モジュールで作ったサンドボックス ]
├── 独自のグローバルオブジェクト (コンテキスト)
└── 独立したスコープチェーン
通常、1つのNode.jsプロセスの中には1つのメインコンテキストしかありません。そのため、どんなに工夫しても同じ実行環境で動く限り、外部からの影響を100%遮断するのは難しいのです。
そこで登場するのが `vm` モジュール です。これを使うと、Node.jsプロセスの中に「V8エンジンレベルで完全に独立したミニ宇宙(サンドボックス環境)」を動的に作り出すことができるようになります。
—
2. `vm.createContext()` で独立したスコープ空間を作ってみよう
百聞は一見に如かずです。実際にNode.jsのコードを書いて、自分だけのスコープ空間(サンドボックス)を作ってみましょう!
特別なライブラリのインストールは不要です。Node.jsがあればすぐに動かせますよ。
// Node.js標準の vm モジュールを読み込みます
const vm = require(‘vm’);
// 1. メイン環境(メインプロセス)で変数を定義してみます
global.mainVariable = ‘私はメイン環境の変数です!’;
let mainLetVariable = ‘メインのlet変数’;
// 2. サンドボックスの中で使いたい「独自のグローバルオブジェクト」の初期状態を用意します
const sandbox = {
sandboxVariable: ‘私はサンドボックス内の変数です!’,
console: console // サンドボックス内でも console.log を使えるように移植しておきます
};
// 3. このオブジェクトを「V8のコンテキスト(独立した実行環境)」に変換します
const context = vm.createContext(sandbox);
// 4. サンドボックス内で実行したいJavaScriptコード(文字列)を用意します
const code = `
// サンドボックス内の変数にアクセス
console.log(sandboxVariable);
// メイン環境のグローバル変数にアクセスを試みる…?
console.log(typeof mainVariable);
`;
// 5. 作成したコンテキスト内でコードを実行!
console.log(‘— サンドボックス内の実行結果 —‘);
vm.runInContext(code, context);
console.log(‘— メイン環境の確認 —‘);
console.log(‘メイン環境の変数:’, global.mainVariable);
実行結果
— サンドボックス内の実行結果 —
私はサンドボックス内の変数です!
undefined
— メイン環境の確認 —
メイン環境の変数: 私はメイン環境の変数です!
コードの解説とポイント
どうでしょうか?
サンドボックス内で `mainVariable` を参照しようとしても `undefined` になっていますよね!
`vm.createContext(sandbox)` を呼び出した瞬間、V8エンジンは渡された `sandbox` オブジェクトを新しいコンテキストのグローバルオブジェクト(トップレベルのスコープ)として再定義します。
メイン環境で定義された変数やスコープチェーンは「隔離の壁」によって完全に遮断され、サンドボックス内のコードからは見えなくなるのです。これが変数の隔離の第一歩になります。
—
3. サンドボックス内で魅せる `var` vs `let/const` の挙動の違い
ここからが、JavaScriptコアの真骨頂であり、最高に面白いところです!
みなさんは、「`var` で宣言したグローバル変数はグローバルオブジェクトのプロパティになるけれど、`let` や `const` で宣言した変数はそうならない」という知識を聞いたことがありますか?
これを `vm` モジュールを使って実験し、視覚的に証明してみましょう。
const vm = require(‘vm’);
// 空のサンドボックス(コンテキスト)を作成します
const sandbox = { console: console };
const context = vm.createContext(sandbox);
// サンドボックス内で var, let, const をそれぞれ宣言するコードを実行します
const code = `
var myVar = ‘varで宣言した値’;
let myLet = ‘letで宣言した値’;
const myConst = ‘constで宣言した値’;
console.log(‘サンドボックス内でのアクセス:’);
console.log(‘myVar:’, myVar);
console.log(‘myLet:’, myLet);
console.log(‘myConst:’, myConst);
`;
vm.runInContext(code, context);
console.log(‘\n— サンドボックスの外(メイン環境)から sandbox オブジェクトを観察 —‘);
console.log(‘sandbox.myVar ->’, sandbox.myVar);
console.log(‘sandbox.myLet ->’, sandbox.myLet);
console.log(‘sandbox.myConst ->’, sandbox.myConst);
実行結果
サンドボックス内でのアクセス:
myVar: varで宣言した値
myLet: letで宣言した値
myConst: constで宣言した値
— サンドボックスの外(メイン環境)から sandbox オブジェクトを観察 —
sandbox.myVar -> varで宣言した値
sandbox.myLet -> undefined
sandbox.myConst -> undefined
この結果が意味する「JavaScriptの超本質」
この実行結果、驚きませんでしたか?
サンドボックス内で実行したコードの中では、`myVar` も `myLet` も `myConst` も問題なく読み取れています。しかし、コード実行後にメイン環境から `sandbox` オブジェクトの中身を見てみると……`myVar` だけがプロパティとして浮き彫りになっていて、`let` や `const` は存在しない(`undefined`)のです!
これは一体なぜなのでしょうか?
【V8コンテキスト内のメモリ空間の概念図】
┌─────────────────────────────────────────────────────────┐
│ コンテキスト (独立空間) │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ グローバルオブジェクト (sandbox オブジェクト) │ │
│ │ – console │ │
│ │ – myVar: ‘varで宣言した値’ <-- varはここに入る │ │
│ └──────────────────────────────────────────────────┘ │
│ ▲ │
│ │ 参照 │
│ ┌────────────────────────┴─────────────────────────┐ │
│ │ 宣言的環境レコード (Declarative Environment) │ │
│ │ - myLet: 'letで宣言した値' <-- let/constはここ │ │
│ │ - myConst: 'constで宣言した値' │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
JavaScriptの仕様(ECMAScript)において、グローバルスコープで変数を宣言したとき:
1. `var` 宣言: グローバルオブジェクト(`Object Environment Record`)の直下プロパティとして登録されます。
2. `let` / `const` 宣言: グローバルオブジェクトのプロパティにはならず、隠蔽された「宣言的環境レコード(Declarative Environment Record)」という領域に保持されます。
`vm` モジュールでコンテキスト化された `sandbox` オブジェクトは、まさに「グローバルオブジェクト」そのものです。だからこそ、`var` で宣言した値だけが `sandbox.myVar` として外側に露出したわけですね。
`let` や `const` がいかに強力に安全なスコープを閉じ込めてくれるかが、この実験からもよく分かりますよね!
—
4. 陥りやすい罠:「完全な隔離」をつくる際の注意点と抜け穴
さて、ここまで学ぶと「`vm` を使えば完全に安全なサンドボックス(ユーザーが投稿したスクリプトを安全に実行する環境など)が作れるぞ!」と思うかもしれません。
ですが、実はここにプログラミング初心者が必ずハマる大きな罠(脱獄の可能性)が存在します。
例えば、サンドボックスに関数やオブジェクトを1つでも渡してしまうと、JavaScriptの「プロトタイプチェーン」を辿って、メイン環境のグローバル空間へと「脱獄」できてしまうケースがあるのです。
脱獄(Context Escape)のコード例
const vm = require(‘vm’);
const sandbox = {
// メイン環境のオブジェクトや関数を安易に渡してしまう
callback: () => { console.log(‘コールバック関数が呼ばれました’); }
};
const context = vm.createContext(sandbox);
// サンドボックス内の悪意のある(あるいは偶然の)コード
const badCode = `
// callback の Constructor(メイン環境の Function)を経由して、
// メイン環境の Function コンストラクタを取得する
const MainFunction = callback.constructor;
// メイン環境のコンストラクタを使って、メインプロセスでコードを実行する関数を作る!
const escapeProcess = MainFunction(‘return process’);
// メイン環境の process オブジェクトを奪取!
const mainProcess = escapeProcess();
mainProcess.mainEnvInjected = ‘サンドボックスから書き換えました!’;
`;
vm.runInContext(badCode, context);
// メイン環境で確認すると…
console.log(‘メイン環境のプロセスの値:’, process.mainEnvInjected);
実行結果
メイン環境のプロセスの値: サンドボックスから書き換えました!
なぜこれが起きてしまうのか?
JavaScriptのすべてのオブジェクトは、自身の「生みの親」であるコンストラクタやプロトタイプ(`__proto__`)への参照を持っています。
渡された `callback` という関数はメイン環境で生成された関数なので、その `.constructor` は「メイン環境の `Function`」を指しています。サンドボックス内のコードはそれを辿って、メイン環境の権限で自由なコードを実行できてしまったのです。
安全にスコープを隔離するための黄金ルール
1. 信頼できない第三者のコードを動かす用途に標準の `vm` モジュールをそのまま使わない(セキュリティ用途には `isolated-vm` などのネイティブC++バインディングライブラリを検討する)。
2. データを渡すときは、参照ではなくディープコピー(JSONシリアライズ等)して渡す。
3. オブジェクトのプロトタイプを極力削ぎ落とす(`Object.create(null)` を使うなど)。
—
5. まとめ:JavaScriptのスコープを支配しよう!
今回の探求で、普段何気なく使っているスコープと変数の仕組みが、ぐっと身近に、そして深く理解できたのではないでしょうか?
今回学んだ要点をまとめますね!
- スコープの本質: 変数にアクセスできる範囲であり、V8エンジン内では「環境レコード」という構造体で管理されている。
- コンテキスト: コードが評価される舞台(グローバル環境)。`vm.createContext()` で独立した舞台を作ることができる。
- `var` と `let/const` の違い: `var` はグローバルオブジェクト(コンテキストそのもの)のプロパティになるが、`let/const` は宣言的環境レコードに守られるため露出しない。
- オブジェクトの参照: オブジェクトを渡すとプロトタイプチェーンを辿って外側の環境にアクセスできるため、スコープの境界線を意識することが極めて重要。
`var`, `let`, `const` の動作の違いや、グローバルオブジェクトとの関係性を「メモリ空間やコンテキスト」の視点から捉えられるようになれば、もうJavaScriptの変数管理で怖がるものは何もありません。
ここをクリアしたあなたなら、モダンなJavaScriptも、Node.jsの高度なアーキテクチャも、自信を持ってバッチリ使いこなせますよ!
また次のステップでも、楽しく本質を学んでいきましょうね!