グローバルオブジェクトとvarの奇妙な関係:windowプロパティへの自動バインドの歴史的経緯とV8の深層
JavaScriptという言語を、単なる「ブラウザを動かすための便利なスクリプト言語」として捉えているうちは、この言語が持つ真の魔力、そして歴史的負債が引き起こすランタイムの歪みに気づくことはできない。
ブラウザのグローバルコンテキストにおいて、なぜトップレベルで宣言した `var` が `window` オブジェクトのプロパティとして吸い上げられるのか。そして、なぜモダンな `let` や `const` ではそれが起こらないのか。この挙動は、単なる「仕様の気まぐれ」ではない。Netscape Navigatorの初期から現代のV8エンジン、そしてTC39の標準化プロセスに至るまでの、言語進化の歴史そのものが刻まれたアーキテクチャの痕跡なのだ。
本稿では、この「奇妙な関係」をV8エンジンの内部構造、隠しクラス(Hidden Classes / Shapes)、およびセキュリティの観点(プロトタイプ汚染とRCE)から徹底的に解剖する。
—
1. 歴史的背景:なぜ `var` はグローバルオブジェクトに憑依するのか
JavaScriptが誕生した1995年、Brendan Eichが設計した当初の言語モデルには、「モジュール」という概念は存在しなかった。すべてのスクリプトは単一のグローバル名前空間にフラットに展開され、実行されていた。
当時、開発者がスクリプト内で変数を宣言する手段は `var` しかなく、関数スコープの外側(すなわちトップレベル)で宣言された変数は、すべて「グローバル変数」として扱われる必要があった。ここで設計上の重大な決定がなされた。「グローバル変数は、グローバルオブジェクトのプロパティと完全に同一視されなければならない」 という仕様である。
var legacyVariable = “ECMAScriptの原罪”;
console.log(window.legacyVariable); // “ECMAScriptの原罪”
console.log(window.legacyVariable === legacyVariable); // true
この仕様は、初期のWebにおいて「HTML上のインラインスクリプトと外部スクリプト間でグローバル状態を共有する」という目的においては極めて直感的だった。しかし、これは言語仕様のパーサとランタイムのスコープ解決メカニズムをグローバルオブジェクトと強く結合させてしまい、のちのエンジニアリングにおいて巨大な足かせとなる。
—
2. V8エンジンの内部挙動:`var` と `let`/`const` のメモリ上での決定的な違い
V8エンジン(あるいは任意の近代JSエンジン)の視点から、トップレベルの `var` と `let`/`const` がどのように処理されているかを低レイヤのコード生成とメモリレイアウトの観点から見てみよう。
V8のスコープ解析(Scope Analysis)とプロパティ化
V8は、コードを実行する前にAST(抽象構文木)を構築し、静的スコープ解析を行う。
トップレベルで `var` を用いて宣言された変数は、V8のパーサによって「グローバルスコープの変数」として登録されると同時に、グローバルオブジェクト(ブラウザなら `window`、Node.jsなら `global`)のプロパティ記述子(Property Descriptor)として登録される。
// V8の内部表現の概念図(プロパティとしての登録)
Object.defineProperty(window, ‘legacyVariable’, {
value: undefined,
writable: true,
enumerable: true,
configurable: false // varによる宣言は原則として再設定・削除不可
});
この挙動により、`var` で宣言された変数は、V8のヒープメモリ上において、グローバルオブジェクトの「通常のプロパティ」と同等のアクセスコスト(ハッシュマップまたはインラインキャッシュのミスを伴う動的ルックアップ)を支払うことになる。
`let` と `const` による「脱・グローバルオブジェクト汚染」
ES2015 (ES6) で導入された `let` と `const` は、この歴史的バグとも言える仕様を断ち切るために設計された。これらはブロックレベルスコープを提供するだけでなく、グローバルオブジェクトのプロパティとして自動バインドされないという決定的な違いを持つ。
let modernVariable = “クリーンな世界”;
console.log(window.modernVariable); // undefined
console.log(modernVariable); // “クリーンな世界”
V8エンジン内部において、トップレベルの `let` や `const` は、グローバルオブジェクトのプロパティとしてではなく、Script Scope(スクリプトスコープ)と呼ばれる、グローバルオブジェクトとは独立した特殊なLexical Environment(レキシカル環境)に格納される。
これにより、グローバルオブジェクトのプロパティ名との衝突(名前空間の汚染)を防ぎ、V8のJITコンパイラ(TurboFan)が変数をレジスタやスタックフレームに効率的にアロケーション(最適化)することが可能になった。`var` のような「いつでも外部から書き換えられるプロパティ」という不確実性が排除されるため、インラインキャッシュ(IC)のヒット率が劇的に向上するのだ。
—
3. ランタイムの防壁を揺るがす:プロトタイプ汚染とグローバルオブジェクトの危険性
`var` が生み出すグローバルオブジェクトへの自動バインド、そしてJavaScriptの動的なプロトタイプチェーンの仕組みは、ひとたび設計を誤ると、サプライチェーン攻撃を通じたリモートコード実行(RCE)の踏み台になり得る。
プロトタイプ汚染(Prototype Pollution)のメカニズム
JavaScriptでは、すべてのオブジェクトが `Object.prototype` を継承している。悪意ある攻撃者が再帰的なマージ関数や不安全なJSONパースの脆弱性を突き、`Object.prototype` に任意のプロパティを注入した場合、それはすべてのオブジェクト、ひいてはグローバルスコープにまで波及する。
// 攻撃者によるプロトタイプ汚染のシミュレーション
const payload = JSON.parse(‘{“__proto__”: {“polluted”: “RCEの布石”}}’);
// 何気なくオブジェクトをマージする処理
function unsafeMerge(target, source) {
for (let key in source) {
if (typeof source[key] === ‘object’ && source[key] !== null) {
unsafeMerge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
}
// 汚染の実行
const config = {};
unsafeMerge(config, payload);
// 恐ろしいことに、グローバルな文脈や標準オブジェクトにも影響が及ぶ可能性がある
console.log({}.polluted); // “RCEの布石”
もし、アプリケーションコード側でグローバル空間や `window` オブジェクトのプロパティを安易に参照・実行している場合(例えば、動的な関数呼び出しやテンプレート評価においてグローバル変数を参照している場合)、このプロトタイプ汚染がトリガーとなり、任意のコード実行(RCE)へとエスカレートする。
`var` によって意図せずグローバルオブジェクトのプロパティとなった変数は、こうした外部からのプロトタイプ汚染や意図しないプロパティ列挙(`for…in` ループなどでの混入)の標的になりやすい。
—
4. モダンアーキテクチャにおける正解:`globalThis` の正しい使い方と環境非依存の設計
ブラウザの `window`、Web Workerの `self`、Node.jsの `global`。JavaScriptのランタイム環境によって、グローバルオブジェクトを指す識別子は長年バラバラであった。これを統一するために導入されたのが `globalThis` である。
環境差異を吸収するユニバーサルな参照
コードがブラウザで動いているのか、Node.jsのサーバーサイドで動いているのか、あるいはEdgeワーカーで動いているのかを意識することなく、安全にグローバルコンテキストにアクセスするためには `globalThis` を用いるべきである。
// どのランタイム環境でも安全にグローバルオブジェクトを参照
const runtimeGlobal = globalThis;
// ポリフィルや環境依存のモジュール検出における正しいイディオム
if (typeof runtimeGlobal.CustomElementRegistry === ‘undefined’) {
// ブラウザ環境ではない、あるいは古い環境のハンドリング
}
チーフアーキテクトからの提言:グローバル汚染の完全な排除
シニアエンジニアとして、大規模なJavaScript/TypeScriptコードベースを設計・運用する上での鉄則を記す。
1. `var` の使用を完全に禁止する
ESLint等の静的解析ツールを用い、`no-var` ルールをエラーとして強制すること。すべての変数は `const`(デフォルト)または `let` で宣言し、スコープを最小限に閉じ込める。
2. IIFEやモジュールシステムの徹底
スクリプトは必ずESModules(ESM)として記述し、ファイル自体を独立したモジュールスコープ(Module Scope)にする。これにより、トップレベルで変数を宣言しても、それはグローバルオブジェクトには一切バインドされなくなる。
3. グローバルオブジェクトへの書き込みを行わない
`window.foo = …` や `globalThis.bar = …` のような、グローバルオブジェクトへの動的なプロパティ付与は、V8の最適化(隠しクラスの遷移)を阻害し、メガモーフィック(多態的)な状態を引き起こすため、パフォーマンスの観点からも厳に慎むべきである。
—
結び
JavaScriptの歴史は、簡易的なスクリプト言語から、ブラウザおよびサーバーサイドを支配する堅牢なエンタープライズランタイムへの進化の歴史である。
トップレベルの `var` が `window` にバインドされるという「奇妙な関係」は、その進化の過程で残された古い遺物にすぎない。ランタイムの挙動を低レイヤから理解し、V8の最適化パスやメモリモデルに逆らわないコードを書くこと。それこそが、真にスケーラブルでセキュアなアプリケーションを構築する唯一の道なのである。