【テクニカル・上級編】ブラウザのグローバルオブジェクト(window/globalThis)と変数宣言の奇妙な関係 – JavaScriptコア文法・モダン言語仕様と変数・関数解析バイブル

グローバル変数の欺瞞:`var`が`window`を汚染し、`let/const`がそれを救うECMAScript仕様の深層

JavaScriptエンジニアの多くは、ブラウザのコンソールで何気なく打つ `var a = 1;` が `window.a` としてアクセスできることを知っている。そして、モダンな `let a = 1;` や `const a = 1;` ではそれが起きないことも知っているだろう。

しかし、なぜそのような挙動の違いが存在するのか。単なる「言語の進化」や「レガシー互換性」という言葉で片付けていないだろうか?

V8エンジン(およびその他のECMAScript準拠ランタイム)の内部、とりわけLexical Environment(レキシカル環境)の構築、V8のHidden Class(隠しクラス)によるインラインキャッシュの最適化、そしてプロトタイプ汚染(Prototype Pollution)を起点としたセキュリティ上の脅威に至るまで、この「変数の宣言とグローバルオブジェクトの奇妙な関係」を仕様書とランタイムの物理層から解剖する。

—

1. グローバル環境の二面性:VariableEnvironment と LexicalEnvironment

ECMAScript仕様(ES2015以降)では、実行コンテキスト(Execution Context)の内部に大きく分けて2つの環境レコード(Environment Record)が存在する。

1. VariableEnvironment (VE): 主に `var` 宣言や関数宣言(Function Declaration)を格納する。
2. LexicalEnvironment (LE): 主に `let`、`const`、クラス宣言などを格納する。

グローバル実行コンテキストにおいて、この2つは全く異なる振る舞いをする。

`var` がグローバルオブジェクトのプロパティになる理由

グローバルスコープにおいて `var` で変数を宣言すると、それはVEに紐づくだけでなく、仕様における Global Environment Record の Object Environment Record コンポーネントにバインドされる。

この Object Environment Record は、その名の通り、内部に「バインディングオブジェクト(Binding Object)」としてグローバルオブジェクト(ブラウザなら `window`)を直接保持している。そのため、`var x = 42;` と書くことは、内部的には `window.x = 42` を定義し、かつそのプロパティの `[[Configurable]]` 属性を `false`(厳格モード以外の場合)に設定する操作と同義なのだ。

var globalVar = “I am var”;
console.log(window.globalVar); // “I am var”
console.log(Object.getOwnPropertyDescriptor(window, ‘globalVar’));
/
出力例(非厳格モード):
{
value: “I am var”,
writable: true,
enumerable: true,
configurable: false // varによる宣言は削除不可(configurable: false)になる
}
/

`let` と `const` が引き起こす「見えない壁」

一方、`let` や `const` は Declarative Environment Record(宣言的環境レコード) に格納される。ここには「バインディングオブジェクト」という概念が存在しない。つまり、V8のヒープ上において、`window` オブジェクトのプロパティマップにキーとして登録されることが物理的にないのだ。

これにより、グローバル汚染を防ぐだけでなく、Temporal Dead Zone(TDZ: 一時的死区間)というランタイム上の安全装置が機能する。

—

2. V8エンジンの内部構造:隠しクラス(Hidden Class / Map)と最適化の明暗

V8エンジンは、動的言語であるJavaScriptをネイティブコードにJIT(Just-In-Time)コンパイルする際、オブジェクトのプロパティアクセスを高速化するために Hidden Class(V8内部用語では `Map`) を用いる。

`var` によるグローバルプロパティのコスト

グローバルオブジェクト(`window`)は、アプリケーションのライフサイクルを通じてあらゆるスクリプトから参照・変更される「巨大なハッシュマップ」になりがちだ。
`var` でグローバル変数を乱用すると、`window` オブジェクトの隠しクラスが頻繁に遷移(Transition)することになる。

// V8の隠しクラスの遷移を引き起こすアンチパターン
var a = 1; // windowのMap A -> Map B
var b = 2; // windowのMap B -> Map C
function foo() {
return a + b; // インラインキャッシュ(IC)がメガモーフィック(Megamorphic)になりやすい
}

グローバルオブジェクトのプロパティアクセスは、ローカル変数やブロックスコープの変数アクセス(レジスタやスタックオフセットで直接解決される)に比べて、圧倒的にコストが高い。V8はグローバルオブジェクトへのアクセスを最適化しようと試みるが、動的なプロパティ追加・削除(`delete window.a` など)が可能な構造であるため、インラインキャッシュ(IC)がヒットしにくく、ポリモーフィックまたはメガモーフィックな状態に陥りやすい。

`let / const` の優位性

グローバルスコープであっても `let` や `const` で宣言された変数は、Declarative Environment Record に直接配置されるため、`window` オブジェクトの隠しクラスを汚染しない。V8のスコープ分析フェーズにおいて、これらはオフセットベースのレキシカル変数として静的に解決され、JITコンパイル時に効率的な機械語へと変換される。

—

3. セキュリティの深層:プロトタイプ汚染(Prototype Pollution)と RCE への道

「たかが `var` と `window` の挙動の違い」と侮ってはいけない。この仕様の差異は、近年のサプライチェーン攻撃やプロトタイプ汚染(Prototype Pollution)において、極めて重大なセキュリティリスクの温床となる。

グローバルスコープとプロトタイプ汚染の交差点

攻撃者がアプリケーションの依存ライブラリ(NPMパッケージなど)の脆弱性を突き、オブジェクトのプロトタイプチェーン(例: `Object.prototype`)を汚染することに成功したとする。

// 攻撃者によるプロトタイプ汚染のシミュレーション
Object.prototype.isAdmin = true;

もしコードベースのどこかで、不安全なグローバル変数への依存や、次のような動的なプロパティ参照が行われている場合、予期せぬ権限昇格やリモートコード実行(RCE)のトリガーとなり得る。

// アプリケーションコード側(脆弱な実装)
var config = {
mode: “production”
};

function evaluateConfig() {
// windowオブジェクト(グローバル)またはスコープチェーンを伝播するプロパティ参照
if (window.isAdmin) {
// 危険な管理者特権機能の実行
executeCriticalSystemCommand();
}
}

`var` によって生成されたグローバル変数は `window` のプロパティであるため、プロトタイプチェーン上の汚染(`window` のプロトタイプである `Window.prototype` や `Object.prototype`)と名前空間が混ざり合うリスクを常に内包する。

サーバサイド(Node.js)における `globalThis` との差異

Node.jsの環境(CommonJSモジュール)においては、ファイルスコープのトップレベルで `var a = 1;` を宣言しても、それは `global`(あるいは `globalThis`)のプロパティにはならない。Node.jsの各ファイルは独立したモジュールラッパー関数(Module Wrapper)の中で実行されるため、トップレベルの `var` はモジュールスコープ(ローカル)に閉じ込められるからだ。

しかし、レガシーなスクリプト読み込みや、ブラウザ・Node.js両対応を謳った肥大化したポリフィルコードにおいて、`this` や `globalThis` を通じた不適切なグローバル変数の書き込み・読み出しが行われている場合、プロトタイプ汚染の被害はランタイム全体へと波及する。

// 危険なグローバル汚染・参照の例
function unsafeSet(key, value) {
// globalThisやwindowに対して無防備にプロパティを設定する
globalThis[key] = value;
}

もし `key` に `__proto__` や `constructor` が渡され、かつそれをサニタイズせずにオブジェクト構築やプロパティ代入に使っていれば、V8のメモリ空間全体のオブジェクトレイアウトが書き換えられ、プロトタイプ汚染からプロパティインジェクション、そして最終的なコード評価(`eval` や `Function` コンストラクタの悪用)を通じた RCE へと直結する。

—

4. チーフアーキテクトからの提言:モダンJavaScriptの防壁の築き方

ランタイムの挙動と仕様の裏側を理解した今、我々シニアエンジニアが取るべき実務上のプラクティスは明白である。

1. `var` の完全な廃止(ESLintによる強制)
コードベースから `var` を一切排除せよ。ESLintの `no-var` ルールを適用し、すべての変数宣言を `const`(デフォルト)および `let`(再代入が必要な場合のみ)に限定する。これにより、意図しないグローバルオブジェクトの肥大化と隠しクラスの不安定化を防ぐ。

2. グローバルオブジェクトへの依存の排除
ビジネスロジック内で `window` や `globalThis` を直接参照・拡張することを禁止せよ。依存性の注入(DI)パターンを採用し、環境依存の値は明示的に関数引数やコンテキストオブジェクトとして渡す設計を貫くこと。

3. プロトタイプ汚染に対する防御的プログラミング
オブジェクトのマージやディープクローンを行う際は、必ず `Object.create(null)` によるプロトタイプを持たないオブジェクトの活用、あるいは `Object.freeze()` やプロパティ名の厳密なホワイトリスト検証(`__proto__`, `constructor`, `prototype` の拒否)を実装に組み込むこと。

JavaScriptは、一見するとお気楽なスクリプト言語に見える。しかし、その下層でうごめくV8のJITコンパイラ、隠しクラス、そしてECMAScriptの仕様が織りなす挙動は、極めて緻密な工学の結晶である。仕様の深部をマスターした者だけが、真に堅牢で高速なフロントエンド・バックエンドシステムを構築できる。

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