グローバル汚染の正体を暴く:`var` と `let`/`const` が `window` オブジェクトと描く決定的な断絶
コードレビューをしていて、いまだにグローバルスコープで `var` を使っているコードを見かけると、私はエンジニアとして背筋が凍る思いがする。
「動いているからいいじゃないか」ではない。その数行の `var` が、V8エンジンのメモリ空間を歪ませ、ブラウザのグローバルオブジェクトと意図しない結合を生み出し、いつか予期せぬ名前空間の衝突という名の爆弾をプロジェクトにもたらす。
今回は、JavaScriptの歴史的呪物である `var` と、モダンな `let`/`const` が、ブラウザのグローバルオブジェクト(`window` / `globalThis`)とどのように結びついているのか。ECMAScript仕様とV8のランタイム挙動の深淵を覗きながら、プロフェッショナルが守るべき設計の鉄則を解き明かしていく。
—
1. なぜ `var` で宣言した変数は `window` のプロパティになるのか?
JavaScriptの初期衝動は「手軽さ」だった。ウェブページに数行のスクリプトを書き込めば動く。その代償として設計されたのが、グローバルスコープにおける `var` と関数スコープの挙動だ。
ブラウザのトップレベル(モジュールとしてではなく、通常のスクリプトタグやグローバルコンテキスト)で以下を実行したとき、何が起きているか。
var appName = “EnterprisePortal”;
console.log(window.appName); // “EnterprisePortal” と出力される
console.log(window.appName === appName); // true
なぜ、変数宣言が `window` オブジェクトのプロパティとして生えてくるのか? 答えはECMAScript仕様の Global Environment Record(グローバル環境レコード) と、ブラウザが実装する WindowProxy / Window の仕様の結合にある。
仕様の裏側:VariableEnvironment と Binding
ECMAScriptの仕様上、グローバルコードが評価される時、V8は `Global Environment Record` を生成する。このレコードは、実は2つの異なるストレージの側面を持っている。
1. Declarative Environment Record(宣言的環境レコード): `let` や `const`、`class` が格納される場所。
2. Object Environment Record(オブジェクト環境レコード): `var` や関数宣言(FunctionDeclaration)がバインドされる場所。
`var` で宣言された識別子は、Object Environment Record に「プロパティ」として直接書き込まれる。そして、ブラウザのランタイム(V8など)は、グローバルな Object Environment Record のバインディングと、JavaScriptの `window` オブジェクト(正確にはグローバルオブジェクト)のプロパティを 同期(Expose) する仕様になっている。
つまり、`var appName = “…”` と書く行為は、実質的に以下を行っているのと同義なのだ。
// 概念的な挙動
window.appName = “EnterprisePortal”;
これが「変数を宣言したつもりが、いつの間にかグローバルオブジェクトのプロパティを汚染していた」という現象の正体である。
—
2. `let` と `const` はなぜ `window` に現れないのか?
では、モダンな `let` や `const` はどうだろうか?
let framework = “React”;
const MAX_RETRY = 3;
console.log(window.framework); // undefined
console.log(window.MAX_RETRY); // undefined
出力は当然 `undefined` になる。ここに `let` と `const` の偉大な進化がある。
`let` や `const` で宣言された変数は、先ほど挙げた Declarative Environment Record に格納される。これはプレーンなJavaScriptのオブジェクト(プロパティの集まり)とは異なり、ECMAScriptの仕様内部でのみ解決されるスロットのようなものだ。
そのため、グローバルオブジェクト(`window`)のプロパティリストには一切露出しない。これにより、以下のメリットが生まれる。
- プロパティ列挙(`for…in` や `Object.keys(window)`)にヒットしない
- 既存のグローバルプロパティ(例えば `window.name` や `window.status` など、歴史的経緯で存在するビルトインプロパティ)との名前衝突を完全に回避できる
- 意図しない外部からの書き換え(プロパティの再代入や削除 `delete window.xxx`)を防げる
テンポラル・デッドゾーン(TDZ)の強靭さ
さらに、`let`/`const` は巻き上げ(Hoisting)は行われるものの、初期化される前にアクセスしようとすると `ReferenceError` を投げる TDZ(Temporal Dead Zone) という防壁を持っている。
`var` のように「巻き上げられて勝手に `undefined` になる」というバグの温床を排除し、コードの実行順序と論理破綻をコンパイル・実行初期段階で検知できるようにしたのだ。
—
3. 【プロダクションコード設計】グローバル汚染を防ぐ堅牢なアーキテクチャ
実務の現場では、レガシーなサードパーティ製スクリプトや、古いバンドラーの設定によって、うっかりグローバルが汚染されるリスクが常に伴う。テクニカルリードとして、チーム全体でこのリスクをゼロにするための設計パターンを提示しよう。
鉄則:常にESモジュール(ESM)または即時実行関数(IIFE)を使う
現代のフロントエンド開発において、コードは必ず `type=”module”` または Webpack / Vite などのモジュールバンドラーを通じてビルドされるべきだ。
ESモジュール環境下では、ファイルのトップレベルで宣言された変数や関数は、モジュールスコープ(Module Scope)に閉じ込められ、決して `window` オブジェクトにはアタッチされない。
// api-client.js (ES Module)
const API_TIMEOUT = 5000; // モジュールスコープ。window.API_TIMEOUT は undefined
export async function fetchUserData(userId) {
// …
}
もし、どうしてもレガシーなスクリプトタグ環境でコードを書かなければならない、あるいはブラウザ拡張機能の開発などでグローバル汚染を完全に防ぎたい場合は、IIFE(Immediately Invoked Function Expression) でスコープを完全に隔離する。
/
- レガシー環境向け:完全なスコープカプセル化パターン
/
(function (global, undefined) {
‘use strict’;
// この内部では var を使おうが何しようが、グローバル(window)は汚染されない
const PRIVATE_CONFIG = {
endpoint: ‘https://api.example.com’
};
function initializeApp() {
console.log(“セキュアな初期化処理が走りました”, PRIVATE_CONFIG.endpoint);
}
// 外部に公開したいAPIだけを明示的にグローバルにアタッチする(必要な場合のみ)
global.MyEnterpriseApp = {
init: initializeApp
};
})(typeof window !== ‘undefined’ ? window : globalThis);
—
4. パフォーマンスとメモリ管理の観点から見た変数宣言
ここで、V8エンジンのメモリ空間(ヒープメモリ)と最適化の視点にも言及しておこう。
V8は、オブジェクトのプロパティアクセスを高速化するために Hidden Class(隠しクラス) や Inline Caching(インラインキャッシュ) という最適化メカニズムを使っている。
もし、グローバルオブジェクト(`window`)に対して動的に `var` で変数を追加したり、グローバルプロパティを乱立させたりするとどうなるか?
グローバルオブジェクトの形状(Shape / Hidden Class)が頻繁に変更・変形することになり、V8のJITコンパイラによる最適化が大きく阻害される(メガモニックな状態に陥る)。
また、スコープチェーンのルックアップにおいても、ローカルスコープやモジュールスコープ内の変数はコンパイル時にレジスタや最適化されたスロットへ直接割り当てられるのに対し、グローバルオブジェクトのプロパティ参照は、プロパティチェーンの探索コストが発生するため、わずかではあるが実行速度のペナルティが生じる。
- `const` はV8に対して「この値は再代入されない」という強力なヒントを与える。 これにより、コンパイラはインライン展開や定数畳み込み(Constant Folding)などのアグレッシブな最適化を行うことができる。
—
まとめ:コードレビューで意識すべきこと
変数宣言一つをとっても、JavaScriptという言語の歴史、ブラウザの仕様、そしてV8エンジンの最適化アルゴリズムが複雑に絡み合っている。
明日からのコードレビューでは、以下のポイントを厳しくチェックしてほしい。
1. `var` はプロジェクトから完全駆逐されているか?(ESLintの `no-var` ルールは必須)
2. グローバルスコープに無駄な変数が露出していないか?(モジュール境界が守られているか)
3. 不用意に `window.xxx` を参照・代入して暗黙的な結合を作っていないか?
言語の仕様を深く理解し、ランタイムを味方につけたコードを書くこと。それこそが、保守性が高く、バグの踏み跡すらない堅牢なプロダクションコードを生み出す唯一の道である。