グローバルオブジェクトの「深淵」を覗く:`var`、`window`、そして環境レコードの非対称性
フロントエンドのアーキテクトとして多くのコードベースを渡り歩いてきたが、いまだに「なぜグローバルスコープで `var` を使うと `window` にプロパティが生えるのか?」という問いに対し、表面的な理解で止まっているエンジニアは多い。
モダンな開発環境では ES Modules やバンドラーがスコープをカプセル化してくれるため、グローバル汚染のリスクは減ったように見える。だが、ブラウザのエンジン(V8等)が内部的にどのように「グローバル環境」を管理しているかを知らなければ、メモリリークや不可解な名前衝突のデバッグで泥沼にハマることになる。
今日は、ECMAScript仕様の深淵に触れつつ、なぜ我々が `var` を葬り去り、`let`/`const` を愛するべきなのかをアーキテクチャの観点から紐解いていく。
—
1. 概念の分離:グローバル環境レコード vs グローバルオブジェクト
多くの開発者が混同しているが、仕様上、「グローバル環境レコード」と「グローバルオブジェクト(window)」は別物だ。
- グローバルオブジェクト (`window`): ブラウザ環境において、実行コンテキストでアクセス可能な、APIやDOMを保持する「入れ物」。
- グローバル環境レコード: 変数宣言(`var`, `let`, `const`, `function`)が実際に格納される、スコープ管理のための内部構造。
なぜ `var` は `window` を汚染するのか?
`var` は歴史的経緯から「グローバルオブジェクトのプロパティとして宣言される」という特殊な仕様を持っている。一方で、ES6で導入された `let` や `const` は、「グローバル環境レコード」に直接バインドされるが、`window` オブジェクトにはプロパティとして追加されない。
// ブラウザコンソールで試してほしい
var x = 10;
let y = 20;
console.log(window.x); // 10 -> varはwindowのプロパティになる
console.log(window.y); // undefined -> letはwindowには付与されない
// アーキテクチャ上の教訓:
// windowへのアクセスは、プロトタイプチェーンの探索コストを発生させる。
// グローバルな名前空間を汚染しないことは、単なる行儀の問題ではなく、
// プロパティ検索の最適化というパフォーマンスの観点からも重要だ。
—
2. 巻き上げと「死の領域」:エンジニアを殺す非同期の罠
`var` の巻き上げ(Hoisting)が引き起こす悪夢は、単なる初期化のタイミングだけではない。非同期処理が絡んだとき、それは重大なバグの温床となる。
`var` は関数スコープであり、ブロックスコープを持たない。これが非同期処理と組み合わさると、クロージャが変数の「最終値」を参照してしまうという、典型的なミスを誘発する。
// 最悪な例: varのスコープ汚染と非同期
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i); // 結果は 3, 3, 3 となる。iはグローバル(または関数)スコープで共有されている
}, 100);
}
// 堅牢なアーキテクチャ: letによるブロックスコープ
// letを使用すると、ループの反復ごとに独立したスコープが生成される
for (let j = 0; j < 3; j++) {
setTimeout(() => {
console.log(j); // 結果は 0, 1, 2 となる。各反復ごとの状態が保持される
}, 100);
}
エンジニアが「なぜ値が保持されないんだ?」と頭を抱えるとき、それは多くの場合 `var` が環境レコードをどのように管理しているかを見落としているからだ。`let` を使えば、エンジン側で各反復ごとのメモリ領域が適切に管理され、競合を防ぐことができる。
—
3. パフォーマンスとメモリ効率の最適化
グローバルオブジェクトに大量の変数を詰め込むことは、エンジニアとして最も避けるべき「アンチパターン」だ。
1. プロトタイプチェーンの肥大化: `window` オブジェクトが肥大化すると、名前解決のたびにプロトタイプチェーンを探索する計算コストが増大する。
2. ガベージコレクションの抑制: グローバルスコープで宣言されたオブジェクトは、ページが閉じられるまでメモリに居座る。不要な参照を保持し続けることで、メモリリークのリスクを大幅に高める。
3. 最適化の阻害: V8などのエンジンは、静的なスコープ(`const` や `let` で宣言された範囲)に対して強力な最適化をかける。`var` による動的なプロパティ追加は、エンジンの最適化パスを無効化し、実行速度を低下させる原因となる。
究極の対策:IIFEからモジュールへ
かつては `(function() { … })()` でスコープを切り分けていたが、現代は ES Modules が標準だ。モジュール化されたコードは、トップレベルスコープが `window` から切り離されるため、グローバル汚染を物理的に防ぐことができる。
—
結論:我々が目指すべきアーキテクチャ
伝説的なフロントエンドエンジニアである君たちに伝えたいのは、「言語仕様の隙間を突くコードを書くな」ということだ。
- `var` は歴史遺産として扱い、コードベースからは一掃せよ。
- `const` をデフォルトとし、再代入が必要な場合のみ `let` を検討せよ。
- `window` オブジェクトは、ブラウザのネイティブAPIを呼ぶためだけの場所であり、君たちのアプリケーションのデータ格納庫ではない。
JavaScriptは柔軟で、寛容な言語だ。しかし、その寛容さは諸刃の剣でもある。エンジンがどのようにメモリを確保し、どのようにスコープを解決しているかを知ることは、単なる知識ではなく、君たちが作るプロダクトをより速く、より壊れにくくするための「武器」になる。
次回のコードレビューでは、`var` の亡霊を見つけ次第、迷わずリファクタリングのメスを入れてほしい。それこそが、プロフェッショナルの矜持だ。

コメント