こんにちは!フロントエンドからNode.jsの深層まで、JavaScriptの生態系を知り尽くしたチーフアーキテクトの私です。
今回は、JavaScriptの歴史が生んだ最も奇妙で、そして少し危険な仕様の一つである「グローバルスコープでの `var` 宣言と `window` オブジェクトの自動バインド」について、その背景とメカニズムを紐解いていきます。
「なぜか知らないけれど、変数が勝手にグローバルオブジェクトのプロパティになってしまう……」そんなモヤモヤを抱えていませんか?ここをしっかりクリアすれば、JavaScriptの変数スコープと実行時コンテキストの基本はバッチリマスターできますよ。
それでは、V8エンジンの裏側の動きまでイメージしながら、一緒に深掘りしていきましょう!
—
1. タイムトラベルの始まり:なぜ `var` は `window` に結びつくのか?
私たちが普段何気なく書いているJavaScript。その歴史の初期、Brendan Eich氏がわずか10日あまりでこの言語を設計したとき、変数の宣言には `var` しか存在しませんでした。
当時のJavaScriptは、今のような複雑なWebアプリケーションを作るためのものではなく、ボタンをクリックしたらアラートが出るような、ほんの数行のスクリプトを動かすための小さなおもちゃ(失礼、軽量言語)でした。そのため、「スクリプトのどこからでもアクセスできる変数」=「ブラウザのグローバル空間(`window` オブジェクト)」という設計が、非常にシンプルで都合が良かったのです。
まずは、実際のコードでその「奇妙な関係」を確認してみましょう。
// ブラウザのトップレベル(関数の中ではなく、一番外側)で var を使って宣言します
var appName = “SuperApp”;
// なんということでしょう!
// 自分で window.appName と書いていないのに、window のプロパティになっています
console.log(window.appName); // ➔ “SuperApp”
// 逆も然りです
window.version = “1.0.0”;
console.log(version); // ➔ “1.0.0” (変数が勝手に生えている!)
このコードを実行すると、`var appName` で宣言した変数が、自動的にブラウザのグローバルオブジェクトである `window` のプロパティとして組み込まれます。
ランタイムの裏側で何が起きているのか?
V8などのJavaScriptエンジンは、コードを実行する際、グローバル実行コンテキスト(Global Execution Context)を作成します。
歴史的な仕様(ECMAScriptの仕様)により、「グローバルスコープで `var` を使って変数を宣言すると、それはグローバルオブジェクトの『拡張可能なプロパティ』として定義されなければならない」というルールが定められました。
つまり、変数を作るという行為が、知らぬ間に `window` という巨大なオブジェクトのプロパティを勝手に増やし続けていたわけです。これは、大規模なアプリケーション開発において「意図しない上書き」や「グローバル名前空間の汚染」という深刻なバグの温床になります。
—
2. モダンJSの救世主:`let` と `const` はこの汚染を防ぐ
「じゃあ、この勝手にプロパティが増える恐怖とはずっと付き合っていかないといけないの?」
安心してください。2015年に登場した ES6(ECMAScript 2015)によって、私たちのコードベースはこの混沌から解放されました。それが `let` と `const` の導入です。
モダンな `let` や `const` をトップレベルで使った場合、挙動がどう変わるのか見てみましょう。
// let や const でトップレベルに変数を宣言してみます
let framework = “React”;
const maxUsers = 100;
// window オブジェクトを確認してみます
console.log(window.framework); // ➔ undefined (おっ、入っていない!)
console.log(window.maxUsers); // ➔ undefined (汚染されていない!)
// 変数としてはもちろん使えます
console.log(framework); // ➔ “React”
素晴らしいですね! `let` や `const` は、グローバルスコープで宣言されても `window` オブジェクトのプロパティにはなりません。これらはスクリプト単位の独立した環境(Declarative Environment Record)に保持されるため、予期せぬグローバル汚染を防ぐことができます。
—
3. 環境の多様化と `globalThis` という究極の解
さて、ここで現代の開発におけるもう一つの問題に直面します。
JavaScriptが動く環境は、ブラウザだけではありません。そう、Node.js です。
Node.jsの世界には、ブラウザでおなじみの `window` オブジェクトは存在しません。代わりに、グローバルオブジェクトの名前は `global` です。さらに、WebWorkerの中では `self`、モダンな一部のランタイムではまた別の名前……と、環境によってグローバルオブジェクトの参照先がバラバラでした。
「ブラウザでもNode.jsでも、環境を意識せずに安全にグローバルオブジェクトにアクセスしたい!」
その願いを叶えるために策定されたのが、`globalThis` です。
`globalThis` の正しい使い方
`globalThis` を使えば、コードが実行されている環境(ブラウザ、Node.js、WebWorkerなど)を自動で判別し、そのプラットフォームの正しいグローバルオブジェクトを返してくれます。
// globalThis を使った環境非依存のグローバル変数へのアクセス
globalThis.apiEndpoint = “https://api.example.com”;
// ブラウザなら window.apiEndpoint と同義
// Node.js なら global.apiEndpoint と同義になります
console.log(apiEndpoint); // ➔ “https://api.example.com”
ただし、ここで一つ重要な注意点があります。
先ほどお話した通り、`let` や `const` で宣言した変数は、相変わらず `globalThis` のプロパティにはなりません。 あくまで明示的に `globalThis.xxx` と代入したもの、あるいは古い `var`(ブラウザ環境のみ)だけがグローバルオブジェクトに紐づきます。
モダンな開発では、グローバル変数をむやみに作るべきではありませんが、もしポリフィルやライブラリの共有データなどでどうしてもグローバルオブジェクトを使いたい場合は、環境差分を吸収する `globalThis` を使うのが今のスタンダードなベストプラクティスです。
—
まとめ:明日から使える実践的プラクティス
ここまでの話を、現場で即座に活かせるルールとしてまとめますね。
1. `var` はもう使わない
- 現代のJavaScript開発において、`var` を使う理由は基本的に存在しません。変数宣言はすべて `const`(基本)、再代入が必要な場合のみ `let` を使いましょう。
2. グローバル空間を汚染しない
- トップレベルであっても、不必要な変数を生やすのは避け、モジュール(ES Modules)を活用してスコープを適切に閉じ込めましょう。
3. 環境に依存するコードでは `globalThis` を選ぶ
- `window` や `global` を直接書くのはやめ、常に抽象化された `globalThis` を利用してマルチプラットフォームなコードを書きましょう。
この歴史的経緯とランタイムの挙動さえ押さえておけば、変数のスコープ周りで迷うことはもうありません。
基礎をしっかりと固めて、より堅牢で美しいJavaScriptコードを書いていきましょう!