生きることは忘れること

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

その2の続き。今日はrealmまわりのもろもろの話と、workerではない普通のスクリプトの場合の話の予定だ。

HTMLにおけるscripting(続き)

realmそしてECMAScriptみたび

realmの話をどこで置き去りにしていたかというと、10.2.4節にある“Run a worker”アルゴリズムの中で呼び出されている“creating a new realm”だ。これは8.1.3.3節“Realms, settings objects, and global objects”の中にリンクされている。アルゴリズムの前にこの節の冒頭を見ると

There is always a 1-to-1-to-1 mapping between realms, global objects, and environment settings objects

と書かれている。realmとglobal object(s)とenvironment settings object(s)が1対1対1対応するということだ。込み入った補足を付け加えておくと

A global object is a JavaScript object that is the [[GlobalObject]] field of a realm.

Note: In this specification, all realms are created with global objects that are either Window, WorkerGlobalScope, or WorkletGlobalScope objects.

ともあり、“realm”のリンク先はECMAScript仕様だ。global objectは WindowWorkerGlobalScope 、それに(前回までは省略していた) WorkletGlobalScope であると書かれており、つまりHTML仕様で定義されているJavaScriptオブジェクトだ。いっぽうenvironment settings objectはというと、リンク先はHTML仕様の8.1.3.2節で、これはECMAScript側には存在せずHTML側にだけ存在する概念だ。

さておき“creating a new realm”アルゴリズムに戻ろう。1ステップ目が

Perform InitializeHostDefinedRealm() with the provided customizations for creating the global object and the global this binding.

となっており、 InitializeHostDefinedRealm() を呼び出している。これはECMAScript仕様の9.6節で定義されている。ここで“with the provided customizations for creating the global object and the global this binding”とあるのは、このアルゴリズムが受け取った引数をそのままさらに渡すということだ。思い出すと、“run a worker”から呼び出されるときは“the global object”を受け取っていた(一方“the global this binding”は受け取っていなかった。オプショナル引数が空だったと思えばいい)。さて、ということでECMAScript仕様の InitializeHostDefinedRealm() の定義だ。

  1. If the host requires use of an exotic object to serve as realm's global object, let global be such an object created in a host-defined manner. Otherwise, let global be undefined, indicating that an ordinary object should be created as the global object.
  2. If the host requires that the this binding in realm's global scope return an object other than the global object, let thisValue be such an object created in a host-defined manner. Otherwise, let thisValue be undefined, indicating that realm's global this binding should be the global object.
  3. Perform SetRealmGlobalObject(realm, global, thisValue).
  4. Let globalObj be ? SetDefaultGlobalBindings(realm).
  5. Create any host-defined global object properties on globalObj.

global objectがexotic objectかどうかで場合分けをするという小難しいことが書いてあるが、 SetRealmGlobalObject(realm, global, thisValue) でrealmにglobal objectがsetされると分かる。定義は9.3.3節にある。

  1. If thisValue is undefined, set thisValue to globalObj.
  2. Set realmRec.[[GlobalObject]] to globalObj.
  3. Let newGlobalEnv be NewGlobalEnvironment(globalObj, thisValue).
  4. Set realmRec.[[GlobalEnv]] to newGlobalEnv.

ここでこれまで散々出てきた [[GlobalEnv]] がsetされている。 それから NewGlobalEnvironment() の定義(9.1.2.5節)を見ると

Set env.[[GlobalThisValue]] to thisValue.

というのがあって、いま追いかけているworkerの場合は thisValue (HTML側の呼び出しでは“the global this binding”と言っていたもの)は指定していなかったから globalObj になっている。この部分で realm.[[GlobalEnv]].[[GlobalThisValue]] にthe global objectがsetされたことになる。始まりに辿り着いたわけだ。ついでにいうと、the global objectとは異なる thisValue すなわち [[GlobalThisValue]] を指定することも可能であることが分かった。

ちなみにrealmとは何かというと、ECMAScript仕様9.3節“Realms”の冒頭には次のようにある。

Before it is evaluated, all ECMAScript code must be associated with a realm. Conceptually, a realm consists of a set of intrinsic objects, an ECMAScript global environment, all of the ECMAScript code that is loaded within the scope of that global environment, and other associated state and resources.

私訳:すべてのECMAScriptコードは、評価される前に、realmと結び付けられる。概念的には、realmは、内在的オブジェクト〔ECMAScript仕様で定義されるオブジェクト〕の集合、ECMAScriptグローバル環境、そのグローバル環境のスコープの中で読み込まれたすべてのECMAScriptコード、その他そのrealmと結び付けられた状態やリソースから成る。

この説明を読んでもぜんぜん意味がわからないが、ざっくり、JavaScriptのスクリプトやオブジェクトが属している箱で、そこにいろいろメタデータがくっついているものだと思えばいいだろう。realmという英単語からすると「土地」のニュアンスだろうか。workerのスクリプトが読み込まれ実行されるときに、realmが作られ、the global objectと結び付けられ、そのrealmのもとでスクリプトが実行される、というわけだ。今まで仕様書を追いかけてきたのは畢竟その過程ということになる。

Webページの読み込みとスクリプト実行

script要素の処理

workerではない普通のスクリプトの話に移る。同じようにthe global objectやrealmを追いかけていく旅になる。エントリポイントはというと、 <script> 要素の定義をしているHTML仕様4.12.1節にしよう。この節の中で“prepare the script element”という長いアルゴリズム(34ステップもある!)や“execute the script element”というアルゴリズムがある。これらのアルゴリズムがいつ呼び出されるかも追いかけた方が本当は良いのだろうが、 <script> 要素のスクリプトが読み込み・実行されるタイミングは async とか defer が絡んで複雑な割に本質的ではないので、省略する。後者の“execute the script element”には

Run the classic script given by el's result.

というステップがある(classicではなくmoduleの場合は例によって割愛)。“Run the classic script”はworkerで見たものと同じで、思い出すと、引数はscriptを1個取り、そのscriptの中にrealmも入っているのだった。その引数は“el's result”と言っている。“el”はelementのことで要素を指す変数名である。つまり要素の(仮想)プロパティである“result”にscriptが格納されているということだから、あとはそれがどんな値か(もっというとどんなrealmを持つか)を探し出せばよい。それが分かるのが“prepare the script element”アルゴリズムで、30ステップ目が

Let settings object be el's node document's relevant settings object.

である。ここで“settings object”変数が定義されており、それは“el's node document's relevant settings object”なのだそうだ。このあとは <script src="..."> のケースなのかインラインスクリプトなのかで分岐があって、前者の場合は31ステップ目の11サブステップで“Fetch a classic script”アルゴリズムにこの変数が渡される。後者の場合は32ステップ目の“creating a classic script”にこの変数が渡される。いずれもworkerの場合に通ったところで、この“settings object”が最終的にscriptのrealm(すなわちglobal object)を決めることはもう分かっている。さあ“el's node document's relevant settings object”とはなんだ。

documentのrealmへ

日本語にすると「要素のノード文書に関連するsettings object」となる。“node document”にはリンクが貼られていて、辿った先はDOM Standardの4.4節である。DOM StandardもWHATWGが策定する仕様で、たとえば身近なところでは addEventListener()querySelectorAll() などはこの仕様で定義されている。HTML仕様はDOM仕様を基盤として構築されていて、HTMLの要素はDOMのノードでもある、という構造になっている。ここでいう文書 (document) とは document (window.document) のことで、HTMLのファイル1個が文書1個に対応するという理解でそう外れていない。“node document”とはノード(要素)が属する文書のことで、自然に一意に決まりそうなのは分かっていただけると思う(もちろん追及しようと思えばできる。ここでは省略)。

次に“relavant settings object”にもリンクが貼られていて、これはさっき見た8.1.3.3節“Realms, settings objects, and global objects”の中に飛ぶ。ここは非常に複雑なのだが、要は document というJavaScriptオブジェクトは、先の議論によればどこかのrealmに属しているはずで、そのrealmのことを指して“relavant”と言っている。realmと(environment) settings objectが1対1対応することを忘れていた読者は思い出しておこう。

The relevant realm for a platform object is the value of its [[Realm]] field.

Then, the relevant settings object for a platform object o is the environment settings object of the relevant realm for o.

ということでdocumentのrealmを探しに行かなければいけないことが分かった。天下りで答えを言ってしまうと、7.5.1節“Shared document creation infrastructure”の“create and initialize a Document object”アルゴリズムにある。これを見つけるのは難しい。8ステップ目の5サブステップが次のようになっている。

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

例の“creating a new realm”アルゴリズムが呼び出されていて、the global obejctが指定されている。ついでに、the global this bindingも指定されている!つまり、workerではない普通のWebページでは実はthe global objectと globalThis は違うものだったのだ(これでwindowの場合とworkerの場合で self の定義の仕方が違った謎も解けた!workerの場合はこれらは同じだからである)。なお、the global this bindingに指定されている WindowProxy とは何かという話はまたしても👻globalThis👻と🌏global🌏と🌝this🌝 - Qiitaに詳しいのでそちらを参照されたい。

このアルゴリズム周りをもう少し探索したいのだが、今日はここまで。続きは気が向いたら。