JavaScript
ECMAScript

👻globalThis👻と🌏global🌏と🌝this🌝

皆さんこんにちは。今回はJavaScriptの👻globalThis👻について解説しようと思います。globalThisはJavaScript (ECMAScript) の新機能です。現在TC39プロセスのStage 3にあり1、このまま何事もなければ近いうちにECMAScript2に正式採用されることになります。

この記事はJavaScript2 Advent Calendar 2018の5日目の記事です。

さて、皆さんはこのglobalThisを使ったことがあるでしょうか。私はありません。というか、現在のところglobalThisが利用可能なのは、つい今日安定版がリリースされたばかりのGoogle Chrome 71のみです。この記事にはglobalThisを使ったコード例が出てきますが、実際に動かしたい場合はちゃんとGoogle Chromeが最新版になっているか確認してくださいね。
(Chrome 71がリリースされるのは日本時間で今日(12月5日)のはずですが、あなたがこの記事を読むタイミングによってはまだ出ていないかもしれません。その場合は出るまで待つかベータ版をインストールしてください。)

globalThisとは何か

一言でいうと、globalThisはグローバルオブジェクトです。グローバルオブジェクトというのは、グローバル変数をプロパティに持つようなオブジェクトです。つまり、グローバル変数を作ると、それがグローバルオブジェクトのプロパティとなって現れるのです。

グローバルオブジェクトの例
// グローバル変数fooを作成(
foo = 123;
// globalThis.fooが123になっている
console.log(globalThis.foo); // 123

// 逆も可能
globalThis.bar = 456;
console.log(bar); // 456

余談ですが、何も付けないでいきなり作った変数とvarを使って宣言した変数はグローバル変数になりますが、letやconstで作った変数はグローバル変数になりません。やっぱりletやconstは偉いですね。

foo = 123;
console.log(globalThis.foo); // 123
var bar = 456;
console.log(globalThis.bar); // 456
let hoge = 7;
console.log(globalThis.hoge); // undefined
const fuga = 8;
console.log(globalThis.fuga); // undefined

ブラウザにおけるグローバルオブジェクト

ところで、ここまで読んだ一部の方は「あれ?」と思ったのではないでしょうか。というのも、皆さんがお使いのブラウザには既にwindowが備わっていますよね。JavaScriptでグローバルオブジェクトといえばwindowのはずです。実際、グローバル変数を作るとwindowのプロパティになることは古くから知られています。

windowの例
foo = 123;

console.log(window.foo); // 123

種を明かしてしまうと、globalThisはwindowと同じです。

console.log(globalThis === window); // true

とはいえ、「じゃあglobalThisとかいらないじゃん」などとは今さら思いませんよね。windowはブラウザ(DOMが実装されている環境)にしかありませんが、今やJavaScriptが動作する環境はそれだけではありません。

node.jsにおけるグローバルオブジェクト

ブラウザ以外のJavaScript環境として代表的なのがnode.jsです。このnode.jsにはwindowは存在しません。

その一方で、node.jsにはもともとglobalがあり、これがグローバルオブジェクトの役割を果たしていました。

node.jsにおけるグローバルオブジェクト
foo = 123;

console.log(global.foo); // 123

まだnodeにglobalThisは実装されていませんが、globalThisを実装するPRを見るに、やはり、というか当然ながら、global === globalThisとなるようです。

thisからグローバルオブジェクトを取得する方法

さて、ここまでで記事タイトルのうちglobalThisとglobalが出てきましたが、thisは何の関係があるのでしょうか。

これもJavaScriptに関してよく知られた事実ですが、トップレベルの(関数の中でない)コードや、関数を普通に(オブジェクトのメソッドとしてではなく)呼んだ場合はthisがグローバルオブジェクトになることが知られています。

thisがグローバルオブジェクトになる例
foo = 123;
console.log(this.foo); // thisがグローバルオブジェクトなのでthis.fooは123
showFoo();

function showFoo() {
  // 関数の中でもthisはグローバルオブジェクト
  console.log(this.foo);
}

ただし、strictモードの場合は関数の中ではthisはグローバルオブジェクトではなくundefinedになるのでこの方法は使えません。

foo = 123;
showFoo();

function showFoo() {
  'use strict';
  // thisがundefinedなのでこれはエラー
  console.log(this.foo);
}

その一方、グローバルの(関数の中ではない)thisはstrictモードでもグローバルオブジェクトとして利用可能です。

<script>
  'use strict';
  var foo = 123;
  // トップレベルのthisはstrictモードでもグローバルオブジェクト
  // として利用可能なので123が表示される
  console.log(this.foo); 
  showFoo();

  function showFoo() {
    // 関数の中ではthisはundefinedなのでエラー
    console.log(this.foo);
  }
</script>

ただ裏技があって、Functionコンストラクタを使うことでstrictモードのコンテキスト内でもstrictモードでない関数を作ることができます。

<script>
  'use strict';
  var globalObj = (new Function('return this;'))();
  console.log(globalObj === globalThis); // true
</script>

しかしそうはいっても、この裏技がうまく動かない環境が少数存在します。具体的にはChrome Appsで使えないそうです3。あと動的に関数を作るのが気持ち悪いですね。

まとめると、グローバルオブジェクトにアクセスする既存の方法は一長一短で、完璧なものはありませんでした。

  • thisはstrictモードの関数内では使用不可。また、実はnode.jsではthisはグローバルオブジェクトではない。
  • (new Function('return this'))()はChrome Appsで使用不可。
  • windowはブラウザ環境のみ。
  • globalはnode.jsのみ。

そしてglobalThis

上のリストの上2つは標準で定められた方法ですが使える状況が限られますし、下の2つはECMAScript標準に含まれていないためは環境によって使えるものが異なり統一されていないというのが現状でした。その一方で、今の時代、全ての環境で共通のJavaScriptコードを動かしたい場面はよくあります。(この概念は時代によって呼ばれ方がいろいろと分かれているような気がしますが、自分はIsomorphic JavaScriptとか呼ぶのがしっくり来ます。)

そこで、ECMAScript標準として統一された方法を用意しようということで登場したのがglobalThisなのです。

globalThisの概念自体は要するにグローバルオブジェクトなので簡単ですね。ただ、これを読んでいるあなたは、👻globalThis👻という名前はどうなのと思ったのではないでしょうか。私は思いました。JavaScriptが嫌いな人たちによるネガティブキャンペーンの対象になっているのをよく見るthisを全面に押し出してくるのはなかなか攻めたネーミングです。そもそも、globalThisという名前を見ても何が言いたいのかよく分かりません。

まあ、とはいえ、気持ちは分からないでもありません。thisはトップレベルの(関数内でない)環境ではグローバルオブジェクトですから、「そのような(グローバルな)環境でのthisを参照できるもの」ということでglobalThisという命名なのでしょう。


ただ、ここでひとつ言えることは、globalThisは実は次善策だということです。もともとは、グローバルオブジェクトを表す変数の名前はglobalにする予定でした。つまり、既に存在していたnode.jsの仕様に合わせようとしたのです。

ところが、実際にglobalを実装してみると、動かなくなるウェブサイトがいくつか発生してしまいました。例えばこのスレッドでは、globalが存在すればnode.js環境であると判断してnode.js向けの処理を行うようなコードが存在することが明らかとなり、それが原因でウェブサイトが動作しなくなった事例が報告されています。
ちょっと本題からずれますが、まったくふざけたコードを書く人もいたものですね。これはglobalを「グローバルオブジェクト」として使うのではなく「node.jsかどうか判定するためのフラグ」として使用するコードを書いたために起こった問題です。globalではなくprocessを使用するコードなども目にしますが将来が危ぶまれますね。

話を戻すと、JavaScriptは後方互換性が重要な言語です。ですから、実際にウェブサイトが壊れてしまう事例がいくつも発生したことでglobalの採用は頓挫し、別の名前を探さなければいけなくなりました。新しい名前に関するわちゃわちゃした議論をGitHubで見ることができます。

これのおかげでglobalの標準化は約2年も遅れたわけですが、何だかんだでglobalThisという名前に決まり、Chrome 71でついに日の目を見ることになったわけです。globalThisという名前についてはプロポーザルに少し説明があります。

After some data-gathering to determine web compatibility of a short list of choices, we settled on globalThis, as this name is both highly likely to be web compatible, and also maps to the concept of being the “global this value” (and not the “global object”, per above).

まあ要するに先ほど述べたような、グローバルなthisの値だからglobalThisなんだよということが書いてありますね。

まとめ

ここまで述べたことはだいたいglobalThisのプロポーザルに書いてあります。既にこれを読んでいた方にとっては新鮮味がない内容だったかもしれませんが、まあそのような方は1行目で既に察したと思うので大丈夫でしょう。

要するに、グローバルオブジェクトを扱う既存の方法はどれも一長一短で統一された方法が無かったので、全ての環境で使えるglobalThisを作りましたということです。現在のところこれが利用可能なのはGoogle Chromeのみですが、他のブラウザやnode.jsも近いうちに追随するのではないかと思います。

とはいえ、実務上は古い環境も考慮しなければならないのでglobalThisだけ使えば他は要らない、というわけにはいかないのがつらいところです。むしろglobalThis対応が加わった結果コードが複雑化するかもしれません。

皆さんももしグローバルオブジェクトを扱う機会があれば、globalThisのことを思い出してあげてください。

余談

今回は余談をまとめの後に持ってきてみました。以降、仕様書という場合はECMAScript® 2018 Language Specificationを指すものとします。(globalThisはES2018には入っていませんがまあ大丈夫です。)

モジュールシステムとグローバルオブジェクト

ところで、グローバル変数って難しい概念ですよね。ブラウザ上のJavaScriptでは、何も宣言せずにいきなり代入された変数や、関数の外でvarで宣言された変数がグローバル変数となります。前者はstrictモードでは使えませんが。

下のコードで確かめてみると、varで宣言した変数fooがグローバルオブジェクトのプロパティとして出現していることが分かりますね。

<script>
  'use strict';
  var foo = 123;
  console.log(globalThis.foo); // 123
</script>

……。

ここで、「いや、違うでしょ」と思った人はかなりJavaScriptに詳しいですね。「当たり前のことを何を今さら」と思った人は修行不足です。

そう、さっきの説明には実は誤っているところがあります。どこが違うのかお分かりでしょうか。

答えは以下のコードです。

<script type="module">
  'use strict';
  var foo = 123;
  console.log(globalThis.foo); // undefined
</script>

なんと、script要素にtype="module"属性を付けたら結果が変わりました。ブラウザ上でのJavaScriptを知っている方は、type="module"というのがimport文を使うときに必要なものであることはお分かりだと思いますが、ECMAScript的にはこれは内部のコードをScriptではなくModuleとして評価するようにする効果があります。実はECMAScriptではプログラムはScriptとModuleの2種類に分類されており、export文やimport文はModuleの中でしか使えない構文です。普通のscript要素で読みこまれるような従来のJavaScriptはScriptです。

そして、トップレベルのvarはScriptではグローバル変数を作る一方、Moduleではそうではないのです。なお、Moduleの中では自動的にstrictモードになるため、無宣言で変数に代入してグローバル変数を作ることはできません。グローバル変数を作りたければ、明示的にグローバルオブジェクトを介する必要があります。

この挙動はよくよく考えてみれば当然ですね。モジュールに分けられたプログラムにおいて、各モジュールは独立している(独立した変数スコープを持っている)べきです。モジュール内部でvarで作った変数がグローバル変数になって気軽に他に影響を与えてもらっては困ります。

言葉だけだと分かりにくいかもしれませんので、例で説明します。index.htmlとmod.jsからなるプログラムを考えましょう。

index.html
<script type="module">
  import { setFoo } from './mod.js';

  var foo = 0;
  setFoo(123456);
  console.log(foo);
</script>
mod.js
var foo;

export function setFoo(value) {
  foo = value;
}

この例で、index.htmlのconsole.logで表示される値はなんでしょうか。

答えは0です。123456ではありません。これはまさに、mod.js内で定義された変数fooがグローバル変数ではない(モジュールのスコープ内の変数になっている)ことの証左であることがお分かりになるかと思います。

このように、モジュールに分けられたJavaScriptではモジュール間で変数のスコープを分けることでモジュール間の独立性を高めています。

ところで、次の実験としてmod.jsを変えて次のようにしてみましょう。

index.html
<script type="module">
  import { setFoo } from './mod.js';

  var foo = 0;
  setFoo(123456);
  console.log(foo);
</script>
mod.js
export function setFoo(value) {
  globalThis.foo = 123456;
}

この場合はconsole.logで何が表示されるでしょうか。

答えはやっぱり0です。「123456だろ」と思った方はちょっと前からもう一度読みなおすといいかもしれません。

仕様書で確かめる

今説明した諸々を仕様書で確かめてみたいという方向けの説明も用意しました。まずScriptにおけるグローバル変数の初期化は15.1.11 Runtime Semantics: GlobalDeclarationInstantiationで行なわれます。ステップ7でVarScopedDeclarationsを、ステップ15でLexicallyScopedDeclarationsを計算していますが、前者がvarで宣言された変数たち、後者がletやconstで宣言された変数たちに相当します。後者の処理はステップ16で行なわれており、流し読みするとenvRec.CreateMutableBindingが呼ばれています。これは要するに、現在のスコープに変数を作成するということです。この「現在のスコープ」は実はグローバルスコープ(グローバルオブジェクトのプロパティとして現れるグローバル変数たちのスコー日)ではないということがポイントです。

一方、varで宣言された変数たちの処理はステップ18で行なわれています。こちらはenvRec.CreateGlobalVarBindingで処理されており先ほどとは違いますね。これを使った場合はグローバル変数(グローバルオブジェクトのプロパティ)として登録されます。ここまでをまとめて例で確かめてみましょう。

<script>
// 実行開始時点で上記のGlobalDeclarationInstatiationは
// 行なわれているので、グローバルオブジェクトに下で宣言されている変数fooが存在
console.log('foo' in globalThis); // true

// var宣言が実際に実行されるまで値はundefined
console.log(globalThis.foo); // undefined

var foo = 123;
// fooに代入されたのでglobalThis.fooも当然書き換わる
console.log(globalThis.foo); // 123

// globalThis.fooへの代入は反映される
globalThis.foo = 99999;
console.log(foo); // 99999

let bar = 345;
// letで宣言した変数はグローバル変数ではない
console.log('bar' in globalThis); // false

// グローバル変数のbarを書き換えてもこの変数barには影響しない
globalThis.bar = 99999;
console.log(bar); // 345

// 逆も同様
bar = -5;
console.log(globalThis.bar); // 99999
</script>

一方でModuleの実行時は何が起こっているでしょうか。それを確かめるには15.2.1.16.4 Instantiate() Concrete Methodから参照されている15.2.1.16.4.2 ModuleDeclarationEnvironmentSetupを見ます。さっきのGlobalDeclarationInstantiationとちょっと似たようなことが書いてありますね。

ただし、varで宣言された変数たちを処理する部分であるステップ12を見ると、CreateGlobalVarBindingではなくCreateMutableBindingが使われていますね。これが先ほどのScriptの場合との違いであり、Moduleの場合にvarで宣言した変数がグローバル変数とはならない(モジュールのトップレベルスコープの変数となる)ことの証左です。

<script type="module">
// 先ほどとは異なり、varで宣言した変数がグローバルオブジェクトに存在しない
var foo = 123;
console.log('foo' in globalThis); // false

// globalThis.fooに何かを代入してもこの変数fooには影響しない
globalThis.foo = 99999;
console.log(foo); // 123
</script>

このように、Moduleではvarで宣言した変数がグローバル変数となりません。このことは注意していないと罠にはまることがあるかもしれません。

node.jsのモジュールシステム

では、仕様書からは離れて次の話題に移りましょう。JavaScriptをばりばり書く方の中でも、前節で取り上げたES Modules(importとかexportとかを使うモジュールシステム)にはあまり馴染みがないという方がいるかもしれません。node.js用のJavaScriptプログラムにおいてはまだまだCommonJS Modulesが現役です。requireを使ってモジュールを読み込むやつですね。node.jsもES Modulesの対応を進めていますが、v10でやっと試験実装が利用可能になった段階です。

そして、node.jsにおける各モジュールは、ScriptかModuleかの分類でいえばScriptとなります。じゃあ、トップレベルvarで変数を作るとどうなるでしょうか。さっそく試してみます。(動作を確認できるようにglobalThisの代わりにglobalを使用しています。)

node.js
console.log('foo' in global); // false

var foo = 123;
console.log(global.foo); // undefined

……?

なんか、先ほどの説明と違いますね。varで宣言した変数がグローバル変数になっていません。

これは実はnode.jsの挙動の特徴です。細かいことは省略しますが、各ファイル(モジュール)は暗黙のうちに関数に囲まれているため、varで宣言した変数はグローバルスコープではなくその関数スコープに属することになります。これにより、各ファイルでvarで宣言した変数がコンフリクトすることが無くなるという利点があるほか、__dirname等の実装にも寄与しているらしいです。


ところで、今回の話題に関連するnode.jsの特徴的な挙動はもう1つあります。それがトップレベルでのthisです。

実は、node.js環境においてはトップレベルの(先ほど説明したようにnode.jsでの各モジュールは関数に囲まれるので厳密にはトップレベルではありませんが)thisはグローバルオブジェクトではありません。その代わりにexportsオブジェクトがthisに入っています。

// これはnode.jsではtrue
console.log(this === exports);

exportsというのはCommonJS Modules由来のものです。まだちゃんと調べていませんが、ES Modulesモードではまた違った結果になることでしょう。Moduleコンテキストのトップレベルではthisはundefinedですから、多分undefinedだと思います。globalThisが聞いて呆れますね。

globalThisはグローバルオブジェクトではない!?

ちょっと話は変わりますが、まとめの直前くらいにプロポーザルから引用した文を再掲します。

After some data-gathering to determine web compatibility of a short list of choices, we settled on globalThis, as this name is both highly likely to be web compatible, and also maps to the concept of being the “global this value” (and not the “global object”, per above).

最後の部分(筆者が太字で強調)に興味深いことが書いてあります。読んでみると、globalThisはグローバルオブジェクトじゃないよという衝撃的なことを述べていますね。なんと、この記事でさんざんglobalThisはグローバルオブジェクトだよと言ってきたのは嘘だったのです。

とはいえ別に実務上なにか問題があるわけではありません。グローバルオブジェクトだと思って安心して使ってください。最後に、これが言いたいのは何かということを解説したいと思います。

aboveのところのリンクの先ではなにやらWindowProxyというワードが出てきていますね。これはブラウザ上のJavaScriptにおけるwindowオブジェクトのことを指しています。

ここでポイントとなるのは、JavaScriptでは他のページ(より正確にはブラウジングコンテキスト)のwindowオブジェクトを取得できるという点です。これには、window.openを使用するとか、iframe要素のcontentWindowを使用するなどの方法があります。また、当然ながら同一オリジンポリシーの影響を受けます。

これはブラウジングコンテキストに対して紐付いているオブジェクトですから、参照されているブラウジングコンテキスト内でページが移動してもWindowオブジェクトは健在です。このことを確かめられるテストページを用意したので、PCの方は開きながら続きを読んでください。また、ソースコードは適宜参照してください。(スマートフォン等だと複数タブを行き来するのが面倒かもしれません。また、ソースコードを見ないと何を言っているかいまいち分からないかもしれません。)

テストページを開くとindex.htmlが表示されます。「foo.htmlを開く」ボタンを押すとfoo.htmlが新しいタブで開かれます。ソースコードを見ると、window.openの結果(foo.htmlのWindowオブジェクト)が(index.html内の)fooWindow変数に代入されることが分かります。

このfooWindow変数は、今開かれたfoo.html内におけるwindowオブジェクトと同じです。そのことは、fooWindowを通じてfoo.html内のグローバル変数を参照したり操作できることから分かります。

foo.htmlを見るとグローバル変数fooに123が代入されていることが分かります。一方でindex.htmlにある「fooの値を表示」ボタンを押すとfooWindow.fooが表示されますが、この結果は123となります。確かにfoo.html内のグローバル変数fooが参照できていますね。

さらに、foo.html内にある「fooの値を変更」ボタンを押すとグローバル変数fooの値が123456になります。その後、index.htmlに戻って再度「fooの値を表示」ボタンを押すと今度は123456が表示されます。このことからも、fooWindow.fooがfoo.htmlの中のグローバル変数fooそのものであることが分かります。

問題はここからです。ブラウザのタブというのはウェブページを表示しており、ウェブページはリンクをたどって別のページに移動することができます。今回は、foo.htmlにはbar.htmlへのリンクが用意してあります。では、bar.htmlに移動してみてください。

皆さんは当然ご存知かと思いますが、別々のページでJavaScriptの実行状態が共有されることはありません。別のページに移動した時点で全ての状態はリセットされます。当然、windowも違うものになるしグローバル変数も全部リセットされます。このことを確かめるためにindex.htmlに戻って「fooの値を表示」ボタンを押してみましょう。そうすると、結果はundefinedとなります。これは、fooWindow.fooが無いことを示しており、bar.html内にグローバル変数fooが無いことと対応しています。

ここで矛盾が発生していることにお気づきでしょうか。index.html内の変数fooWindowは常に同じドキュメントです。これは、index.htmlにfooWindowに再代入するコードが無いことから明らかです。

それにも関わらず、foo.htmlからbar.htmlに移動した時点でfooWindow.fooの値が変わりました。つまり、fooWindowはfoo.htmlのグローバルオブジェクトだったのに、bar.htmlのグローバルオブジェクトに変化したということです。これは、foo.htmlとbar.htmlは別々のページなので別々のグローバルオブジェクトを持つという事実と矛盾しています4。

要約すると、index.htmlから見るとfooWindowに入っているオブジェクトは変わっていないのに、実際はページ遷移の過程で別のグローバルオブジェクトに変わっているという矛盾です。

この矛盾を解消するために導入されるのがWindowProxyです。WindowProxyは内部にWindowオブジェクトへの参照を持っていて、基本的にWindowProxyへの操作は内部のWindowへの操作となります5。今回の例ではfooWindowはWindowProxyであり、最初は内部でfoo.htmlのWindowオブジェクトとつながっています。ですから、fooWindowからの読み込みはfoo.htmlのWindowオブジェクトからの読み込みとなりなります。

foo.htmlがbar.htmlにページ遷移すると、fooWindowが内部に持っているWindowオブジェクトへの参照がbar.htmlのWindowオブジェクトへと差し替わります。これはあくまで内部的な動作であり、fooWindowのWindowProxyオブジェクトとしての同一性は保ったままです。しかし、この時点でfooWindowへの操作は内部のWindowオブジェクト、すなわちbar.htmlのWindowオブジェクトへの操作となるため、実際に操作されるWindowオブジェクトが最初と異なっています。

この2層構造により、前述の矛盾が解消されることになります。結局のところ何が言いたかったのかというと、「windowというのは実はWindowProxyのことであってグローバルオブジェクトたるWindowではないから、今回作るglobalThisをグローバルオブジェクトと呼ぶのは憚られる。なのでglobalThisになった」という話でした。

長々と説明しましたが、なんだかとてもややこしいですね。ここでちょっと話を戻して、globalThisのプロポーザルから一文引用します。

ES6/ES2015 does not account for the Window/WindowProxy structure, and simply refers to ”the global object” directly. This specification does the same.

(超意訳) なんかHTMLの人たちはWindowとかWindowProxyとか言ってるけど別にECMAScript的にはそういうのどーでもいいんで、面倒くさいから単に「グローバルオブジェクト」ってことにしてます。

というわけで、一応名前決めるときに気をつけたとはいえ、全くもってどうでもいい話でした。まさに余談ですね。


  1. 詳細は他のもっと詳しい記事に譲りますが、ECMAScriptに新機能が追加されるにはStage1 〜 Stage 4という4つの段階を経る必要があります。Stage 3は、機能の仕様が固まり、ブラウザ等に試験的に実装されるのを待っている状態です。 ↩

  2. 今さらな説明ですが、ECMAScriptというのはざっくり言うとJavaScriptを策定している標準仕様の名前です。この記事では、ブラウザとかnode.jsとかで動く実際のJavaScriptと対比して、仕様として定められたJavaScriptの挙動に触れたいときにECMAScriptという言葉を使っています。 ↩

  3. 。ただ、Chorme Appsは今はChrome OSのみが対象らしいですが。 ↩

  4. この事実については理由を説明していませんでしたが、HTMLの仕様でそのように定義されています。別のHTML文書へのナビゲーションが発生すると、HTML文書をロードする過程で新しいDocumentが作成され、その過程でグローバルオブジェクトとして新しいWindowオブジェクトが作られることが明記されています。 ↩

  5. ただし、オリジンをまたぐ場合は制限がかかります。これはWindowProxyの定義で規定されています。 ↩