序:コードレビューの現場から
「なぜ、このレガシーなコードでバグが起きるのか、お前は説明できるか?」
テックリードとしてコードレビューを行っていると、未だにグローバルスコープで `var` を使っているコードに遭遇する。そして、それが引き起こす不可解なバグ(暗黙のグローバル変数の汚染、意図しないシャドーイング、非同期処理での値の化けなど)に頭を抱え、デバッガーの前で立ち尽くすジュニアエンジニアの姿を見る。
「とりあえず動くから」という理由で書かれた `var` の多用や、トップレベルでの宣言は、JavaScriptという言語の歴史的負債そのものだ。V8エンジンがどのようにメモリを割り当て、ブラウザのグローバルオブジェクトとどう結びついているのか。そのメカニズムを理解していなければ、堅牢なモダンWebアプリケーションの設計など到底おぼつかない。
今回は、JavaScriptの暗部とも言える「`var` とグローバルオブジェクトの奇妙な関係」を解き明かし、モダンな開発環境においてなぜこの悪習を断ち切らなければならないのかを、V8エンジンのランタイム挙動を踏まえて徹底的に解説しよう。
—
1. なぜ、トップレベルの `var` は `window` のプロパティになるのか?
JavaScriptの初期(Netscape Navigatorの時代)に遡ろう。当時、言語には現在の私たちが知ような「モジュールシステム」も「ブロックスコープ」も存在しなかった。すべてのスクリプトは単一のグローバル空間で実行され、網の目のように関数や変数が絡み合っていた。
ここで、言語仕様(ECMAScript)とホスト環境(ブラウザ)の歴史的経緯が絡む。
スクリプトスコープとグローバルオブジェクトの乖離
ブラウザ環境において、グローバルオブジェクトである `window`(あるいはWorker環境の `self`)は、DOMAPIやBOMのエントリポイントであり、すべてのグローバル変数が収まる「容器」として設計された。
ここで `var` を使って変数を宣言したとき、V8エンジン内部で何が起きているか。
// ブラウザのグローバルコンテキスト(トップレベル)で実行
var legacyVariable = “污染の始まり”;
console.log(window.legacyVariable); // “污染の始まり”
console.log(window.legacyVariable === legacyVariable); // true
ECMAScriptの仕様上、関数外(グローバルスコープ)で宣言された `var` は、グローバル環境レコード(Global Environment Record)の「変数宣言(Variable Declaration)」としてバインドされる。そしてブラウザの仕様(HTML Living Standard)により、グローバル環境レコードの変数オブジェクトは、グローバルオブジェクト(`window`)のプロパティと直結(ビュースルー)するという仕様が規定されたのだ。
これが、「トップレベルの `var` が `window` のプロパティになる」根本的な理由である。
`let` と `const` はこの呪縛を断ち切った
ES2015(ES6)で導入された `let` と `const` は、この歴史的過ちを修正するために設計された。
let modernVariable = “安全な変数”;
const CONSTANT_VALUE = “不変の正義”;
console.log(window.modernVariable); // undefined
console.log(window.CONSTANT_VALUE); // undefined
`let` や `const` は、グローバルスコープで宣言されてもグローバルオブジェクトのプロパティにはならない。これらは「スクリプトスコープ(Script Scope)」というV8内部の独立した領域に格納され、`window` を汚染しない。これにより、DOMのプロパティ名(例えば `name`, `status`, `location` など)とグローバル変数の衝突という、往年の開発者を悩ませた地獄のようなバグが過去のものとなった。
—
2. V8エンジンのヒープとプロパティ汚染の恐怖
「`window` のプロパティになるからといって、何が問題なのか?」
そう思うかもしれない。しかし、ブラウザのグローバルオブジェクトは何千もの標準API(`fetch`, `document`, `setTimeout` など)がぶら下がる巨大な共有ミュータブルステートである。
ここに `var` で変数を生やす行為は、パブリックなオブジェクトのプロパティ空間に不法投棄するようなものだ。さらに、V8エンジンの最適化の観点からも最悪のアンチパターンとなる。
隠れクラス(Hidden Classes / Shapes)の破壊
V8は、オブジェクトのプロパティアクセスを高速化するために「隠れクラス(Shapes)」という仕組みを使い、プロパティのオフセットを最適化する。グローバルオブジェクト(`window`)はアプリケーションのライフサイクルを通じて膨大な動的変更を受けるため、V8にとっても最適化が難しい特殊なオブジェクトである。
そこに `var` を通じて動的にプロパティを追加・変更し続けることは、V8のインラインキャッシュ(IC)をミスさせ、プロパティルックアップを遅延させる原因(メガモーフィックな状態への陥落)になり得る。パフォーマンスを極限まで絞り出すフロントエンドにおいて、グローバル領域の汚染はメモリとCPUサイクルの無駄遣いだ。
—
3. 環境の差異を克服する:`globalThis` の正しい使い方
フロントエンド(ブラウザ)とバックエンド(Node.js)、あるいはWeb WorkerやService Workerなど、JavaScriptが実行されるホスト環境は多様化している。
従来、グローバルオブジェクトを参照するためには以下のような環境分岐を書く必要があった。
// 悪夢のような環境判定コード
const getGlobal = function() {
if (typeof self !== ‘undefined’) return self;
if (typeof window !== ‘undefined’) return window;
if (typeof global !== ‘undefined’) return global;
throw new Error(‘unable to locate global object’);
};
このようなボイラープレートを書く必要は、もはやモダンな開発には存在しない。ECMAScript 2020で導入された `globalThis` を使えば、実行環境を意識することなく、標準化された方法でグローバルオブジェクトにアクセスできる。
// すべての環境(ブラウザ、Node.js、Worker)で一意にグローバルオブジェクトを指す
globalThis.appConfig = {
apiEndpoint: “https://api.example.com”,
timeout: 5000
};
console.log(globalThis.appConfig.timeout);
しかし、留意してほしい。`globalThis` を通じたグローバル変数の乱用も、結局のところグローバルステートへの依存(Spaghetti State)を生むという点において、`var` の問題と根っこは同じである。
プロダクションコードにおいては、状態はカプセル化され、明示的にモジュール間で受け渡されるべきだ。
—
4. プロダクションコード:堅牢な設定管理とスコープ設計の模範
では、実務の現場でどのようにスコープを設計し、グローバル汚染を防ぐべきか。
ESモジュール(ESM)を前提とした、保守性が高くテスタブルなモジュール設計のコード例を示す。
以下のコードは、アプリケーションの設定情報を安全に管理し、意図しないグローバル汚染を完全に排除したプロダクション品質の設計パターンである。
/
- @fileoverview アプリケーションの設定管理モジュール
- @author Technical Lead
/
// モジュールスコープ(ファイルスコープ)で完結させる。
// 外界からは一切アクセスできない、真にカプセル化されたプライベート状態。
const DEFAULT_CONFIG = Object.freeze({
API_TIMEOUT: 3000,
MAX_RETRIES: 3,
DEBUG_MODE: false
});
let runtimeConfig = { …DEFAULT_CONFIG };
/
- 設定を安全にマージして更新する(イミュータブルな設計)
- @param {Partial
} newConfig
/
export function updateConfig(newConfig) {
// グローバルを汚染せず、ローカルなスコープ内でのみ状態を更新
runtimeConfig = Object.freeze({
…runtimeConfig,
…newConfig
});
console.info(‘[ConfigManager]: 設定が更新されました’, runtimeConfig);
}
/
- 現在の設定を取得する(読み取り専用のコピーを返却)
- @returns {typeof DEFAULT_CONFIG}
/
export function getConfig() {
return runtimeConfig;
}
/
- 必要に応じて、環境に応じたフォールバックを globalThis から安全に取得する例
- (例:SSR時のHTMLインライン設定の読み込みなど)
/
export function hydrateConfigFromHost() {
// globalThisからの安全な値の読み出し(存在しない場合のフォールバック付き)
const hostProvidedConfig = globalThis.__INITIAL_CONFIG__;
if (hostProvidedConfig && typeof hostProvidedConfig === ‘object’) {
updateConfig(hostProvidedConfig);
}
}
このコードの優れたポイント
1. ESモジュールによるスコープ隔離: このファイル内で宣言された変数(`DEFAULT_CONFIG`, `runtimeConfig`)は、グローバルオブジェクトには一切バインドされず、モジュールスコープに閉じ込められる。
2. `Object.freeze` によるイミュータビリティ: 意図しない値の書き換えを防ぎ、バグの温床を断つ。
3. 安全な `globalThis` の活用: サーバーサイドレンダリング(SSR)やHTMLのインライン埋め込みデータから設定をハイドレーションする際も、例外を起さずに安全にフォールバック処理を行っている。
—
結:レガシーな思考からの脱却
`var` が自動的に `window` のプロパティになるという仕様は、JavaScriptの歴史的な名残りであり、現代の厳密なアプリケーション開発においては「排除すべき技術的負債」である。
テクニカルリードとしての私からの指示は明確だ。
- コードベースから `var` を完全に駆逐し、`const` と `let` を適切に使い分けること。
- グローバルオブジェクトへの安易なプロパティ追加を禁止し、ESモジュールによるカプセル化を徹底すること。
- 環境差異を吸収する必要がある場合は、ハックを使わず標準の `globalThis` を用いること。
言語の奥底にあるメカニズムを理解し、V8エンジンが微笑むような、美しく堅牢なコードを書き続けろ。それがプロフェッショナルなWebエンジニアの仕事である。