生きることは忘れること

JavaScriptの window と self と globalThis の話(その2)

その1の続きです。

HTMLにおけるwindowとworkerのインタフェース定義

HTMLの仕様は紆余曲折あるのだが、いまはWHATWGという団体が策定している“HTML Standard”という文書である。これは常に更新され続ける“Living Standard”と呼ばれる形式を取っていて、「バージョン」の概念を持たない。なお、HTMLの仕様はかつてはW3Cが策定していたが、そのあたりのことは1.6節“History”などを参照されたい(日本語ではHTML Living StandardとHTMLの歴史 - とほほのWWW入門がよくまとまっているようだ)。

さて window であるが、これはWebブラウザ特有の機能であり、つまりECMAScript仕様でいう“host”が定義する機能であるから、HTMLの仕様書で定義されていると考えられる。実際7.2.2節“The Window object”という節がある。ここにはWeb IDLを用いたインタフェース定義が書かれている。

[Global=Window,
 Exposed=Window,
 LegacyUnenumerableNamedProperties]
interface Window : EventTarget {
  // the current browsing context
  [LegacyUnforgeable] readonly attribute WindowProxy window;
  [Replaceable] readonly attribute WindowProxy self;
  [LegacyUnforgeable] readonly attribute Document document;
  attribute DOMString name;
  ...
}

これの解説をするのだが、その前にその下の本文を見ると windowself の値が定義されていて

The window, frames, and self getter steps are to return this's relevant realm.[[GlobalEnv]].[[GlobalThisValue]].

と書かれている。 realm.[[GlobalEnv]].[[GlobalThisValue]] はECMAScript仕様にあった globalThis の定義と同じものであることが分かる。realmとは何かという話は例によって省略(今回は後で少し出てくる)。

それでこいつがECMAScript仕様でいうthe global objectであることの説明をしたいのだが、そのためにはWeb IDLの話をしなければいけない。IDLはInterface Description Languageの頭字語で、そのまんまインタフェースを記述する言語ということだが、なかでもWeb IDLはHTML仕様(や関連するWeb分野の仕様)に出てくるJavaScriptのオブジェクト/クラスのインタフェースを定義するための記法である。Web IDL自身はこれまたWeb IDL StandardというWHATWGが策定する仕様で定義されている。HTML仕様では2.1.9節“Dependencies”でWeb IDLへの参照が明記されている。

Web IDLの見た目は普通のプログラミング言語でよくあるインタフェース定義の記法なのでいちいち解説はしないが、 interface Window という Window を定義する行の上にある [ ] で囲まれた部分に着目する。これはWeb IDLの用語でattribute(s)と呼ばれているもので、その定義対象の振る舞いを記述するものだ。Rust言語にも同じ場所に書くattribute(s)というものがあるし、JavaScriptのデコレータなども近い機能であるので、これらに馴染みがあれば理解しやすいだろう。 Window に付けられているattributeの1つ目、 Global=Window というやつがいかにもそれっぽい。定義を探すとWeb IDL仕様の3.3.8節が“[Global]”でまさにそれだ。

If the [Global] extended attribute appears on an interface, it indicates that objects implementing this interface will be used as the global object in a realm.

と、ド直球で書かれている。なお、Web IDL自体はプログラミング言語を特定せずにインタフェースを記述できるものだが、3章は“ECMAScript binding”というECMAScriptで実装するとき特有のことを述べる章なので、ECMAScriptの用語がバリバリ使われている。

なお、workerの場合はというと、workerの種類ごとに、HTML仕様の10.2.1.2節“Dedicated workers and the DedicatedWorkerGlobalScope interface”10.2.1.3節“Shared workers and the SharedWorkerGlobalScope interface”、それにService Worker仕様(これはW3Cが策定している)の4.1節“ServiceWorkerGlobalScope”でそれぞれ[Global] attributeが付与されたインタフェースが定義されている。 self の定義だが、これらのインタフェースはすべてHTML仕様の10.2.1.1節“The WorkerGlobalScope common interface”で定義される親インタフェースを継承していて、そこに置かれている。

The self attribute must return the WorkerGlobalScope object itself.

こちらは自身を返すという定義になっている。定義の仕方が異なる理由はなぜなのだろう。

HTMLにおけるscripting

workerのスクリプト実行

さてこれでだいたい良い気もするのだが、もう少し旅を続けたい。ここまでで、HTML仕様がthe global objectになる(なり得る)インタフェースをきちんと定義していることが分かったが、ではそれらはどのように使い分けられているのだろう。言い換えると、スクリプト内のどの場所でどのthe global objectが使われるのだろう。もちろん普通のスクリプトであれば Window であり、workerとして読み込んだscriptなら該当するworkerのインタフェースなのだが、それはどこに書かれているのか。

HTML仕様におけるスクリプト処理の記述は、7章“Loading web pages”と8章“Web application APIs”に散らばっていて非常に難しい。今回はこの記事のテーマに沿って適当にエントリポイントを設定して辿っていこう。workerの方がまだマシなのでそちらから行く。エントリポイントは10.2.6.3節“Dedicated workers and the Worker interface”で、ここで Worker インタフェース(workerの外側のインタフェース)や new Worker() コンストラクタのアルゴリズムが定義されている。このコンストラクタがworkerのスクリプトを読み込み実行するのでエントリポイントとして明瞭だ。アルゴリズムは8ステップあるが、7ステップ目が

Run a worker given worker, worker URL, outside settings, outside port, and options.

というもので、“Run a worker”というのが内部リンクになっている。別のアルゴリズムの呼び出しだ。リンクを辿って10.2.4節“Processing model”を見よう。5ステップ目に次のようにある。

Let realm execution context be the result of creating a new realm given agent and the following customizations:

For the global object, if is shared is true, create a new SharedWorkerGlobalScope object. Otherwise, create a new DedicatedWorkerGlobalScope object.

ここで“creating a new realm”はこれまた内部リンクなので別のアルゴリズムだが、とにかくrealmを作っている。realmからは逃げられない。その際の引数としてthe global objectを指定しており、先ほどみてきた SharedWorkerGlobalScopeDedicatedWorkerGlobalScope が渡されている。きちんと追うとこの指定されたthe global objectを持つrealmが作られていることが分かるのだが、そっちの追究は後回しにしていったんアルゴリズムの続きにいく。2ステップ先の7ステップ目では

Set up a worker environment settings object with realm execution context, outside settings, and unsafeWorkerCreationTime, and let inside settings be the result.

と言っている。“Set up a worker environment settings object”がアルゴリズムの名前で“with”以下が引数だ。“realm execution context”がついさっき作ったrealmが格納されている「変数」の名前である(原文は斜体になっているのでいくぶん分かりやすい)。ここでenvironment settings objectというものを作っていて、問題のrealmはそのオブジェクトのプロパティとして格納されている(ことがリンクを辿ると分かる)。“let”以下は代入で、“inside settings”が変数名(原文では斜体になっている)、resultつまりset upした結果のオブジェクトを格納している。続きに行って12ステップ目。

Obtain script by switching on the value of options's type member:

JavaScriptにはmoduleスクリプトとそうでないものがある。それによって呼び出すアルゴリズムが分かれているが、とにかくスクリプトをfetchする。つまりURLから取ってくるわけだ。そして“given”以下で渡されている引数に先ほどの“inside settings”が入っている。ここではclassicの方を見ることにして、“Fetch a classic worker script”のリンクを辿り(8.1.4.2節)そのアルゴリズムの最後に行くと

Let script be the result of creating a classic script using sourceText, settingsObject, response's URL, and the default classic script fetch options.

Run onComplete given script.

となっている。“Let script be”の“script”は変数名で、“creating a classic script”がアルゴリズム名だ。それからアルゴリズム冒頭にある引数定義の引用を省略したが、実は“settingsObject”が実は先ほど渡した“inside settings”の内側の名前(仮引数名)である。ここまでで、the global obejctを持つrealmを持つenvironment settings objectを持つscriptが作られたことになる。次の“onComplete”でその“script”を使っているが、これは実はコールバック引数だ。呼び出し元で“with onComplete ... as defined below”と書かれていた。そのコールバックのアルゴリズムも長いが、10ステップ目に

If script is a classic script, then run the classic script script. Otherwise, it is a module script; run the module script script.

というのがある。例によってclassicとmoduleで場合分けされているが同じくclassicの場合だけを追う。念のため書いておくと“classic script script”はtypoではなく、2個目のscriptは変数名(原文では斜体)でいわばインスタンス、“classic script”の方はいわば型の名前、日本語にすると「scriptというclassic script」という意味だ。それで“run the classic script”にリンクが貼られていて、アルゴリズムが呼び出されているということなので、またリンクを辿ってそこを見ればいい。飛んだ先は8.1.4.4節“Calling scripts”だ。6ステップ目、

Otherwise, set evaluationStatus to ScriptEvaluation(script's record).

ScriptEvaluation() のリンク先はHTML仕様を離れてECMAScript仕様に行く。“script's record”は、HTML仕様の“script”はいろいろとメタデータの入ったラッパーオブジェクトで、ECMAScript仕様に渡せる実体が“record”プロパティに入っていると思えばいい。

ECMAScriptふたたび

ScriptEvaluation はECMAScriptの16.1.6節である。最初の方は次のような具合だ。

  1. Let globalEnv be scriptRecord.[[Realm]].[[GlobalEnv]].
  2. Let scriptContext be a new ECMAScript code execution context.
  3. Set the Function of scriptContext to null.
  4. Set the Realm of scriptContext to scriptRecord.[[Realm]].
  5. Set the ScriptOrModule of scriptContext to scriptRecord.
  6. Set the VariableEnvironment of scriptContext to globalEnv.
  7. Set the LexicalEnvironment of scriptContext to globalEnv.
  8. Set the PrivateEnvironment of scriptContext to null.

scriptRecord が引数で、その中から [[Realm]] を取り出している。それから scriptContextglobalEnv をsetしているが、トップレベルのコンテキスト(スコープ)がここで定義されていることになる(実は前回の記事で誤魔化していたところだ)。途中をちょっと飛ばして

Set result to Completion(Evaluation of script).

が終点である。この“Evaluation of script”というのは本当にそのままの意味だ。 scriptscriptRecord.[[ECMAScriptCode]] なのだが、これは“The result of parsing the source text of this script. ”で(16.1.4節)、つまり構文解析の開始記号を評価して結果として値を返すということである。これがJavaScriptを実行している箇所であると言える。はじめから終わりまで辿りきった。

以上で一区切りなのだが、途中で置き去りにしたrealmまわりのもろもろを回収しなければいけない。それにworkerではない普通のスクリプトの場合の話もまだできていない。その3に続く(なんと完結しなかった)!