Opt Technologies Magazine

オプトテクノロジーズ 公式Webマガジン

ECMAScript の Boolean(value) の仕様(の一部)を読む

アイキャッチ用のオプトテクノロジーズのロゴ

ECMAScript の Boolean(value) の仕様の前半部分をなるべく詳細に読んでみました。

はじめに

Webアプリケーション開発者の兵頭(@fhiyo)です。 みなさんは普段の Web 開発の中で Web 標準の仕様書を参照しているでしょうか。正直に言いますと私はあまり見ていなかったのですが、最近は仕様に当たることの重要性について認識を改め意識的に読もうとしています。 ただ私のように Web 標準の仕様を参照することに慣れていない人間にとっては仕様書の読解は労力のかかる作業です。理解が進まない原因の1つとして、見慣れない表現や記法が多く前提知識が不足していることが挙げられると思いました。そこで今回は1つの題材に絞ってなるべく詳細に ECMAScript の仕様書である ECMA-262 を読んでみて、その過程で得た知識を整理して前提知識を補おうと思います。

今回は Boolean(value) のアルゴリズムの詳細を追っていくことにします。ただし、この関数はたったの5ステップなのですがそれでも細かく解説しようとすると長くなってしまうので、分岐で return が行われる2ステップ目までを対象とします。

なお、本記事では ECMAScript® 2023 Language Specification を参照し、本文内で仕様書と言うときはこれを指すものとします。

Boolean(value) の仕様を(アルゴリズムの2ステップ目まで)読んでいく

Boolean(value) の仕様は以下のようになっています。

This function performs the following steps when called:

  1. Let b be ToBoolean(value).
  2. If NewTarget is undefined, return b.
  3. Let O be ? OrdinaryCreateFromConstructor(NewTarget, "%Boolean.prototype%", « [[BooleanData]] »).
  4. Set O.[[BooleanData]] to b.
  5. Return O

上述した通り本記事では2ステップ目まで見ていくことにします。

(Boolean(value)) 1. Let b be ToBoolean(value)

この "Let x be someValue" という表現は仕様書内でよく見かける表現です。この表現については 5.2 Algorithm Conventions に説明があります。

Algorithm steps may declare named aliases for any value using the form “Let x be someValue”. These aliases are reference-like in that both x and someValue refer to the same underlying data and modifications to either are visible to both.

xsomeValue に割り当てる (JavaScrirpt のコードで表すならば let x = someValue のような操作をする) ということです。参照なので、 xsomeValue のどちらかの値が変更されると他方の値も変わります。また既に値が割り当てられている xsomeOtherValue に変更するときは "Set x to someOtherValue" という表現を使います。

よって、

Let b be ToBoolean(value).

という処理は b という変数に ToBoolean(value) という処理の返り値を割り当てるんだな、と考えれば良いです。

抽象操作

ところでこの ToBoolean(value) ですが、仕様書では抽象操作 (Abstract Operation) と呼ばれるものになります。これは仕様書内でのみ使用される関数で、ECMAScript の実装をする JavaScript エンジンでは関数として実装される必要はないものです。別の箇所から参照しやすくするために便宜的に名前を付けた処理の塊だと思えば良いです。

ToBoolean(argument)

では、ToBoolean(argument) という抽象操作のアルゴリズムを見ていきます。

  1. If argument is a Boolean, return argument.
  2. If argument is one of undefined, null, +0𝔽, -0𝔽, NaN, 0, or the empty String, return false.
  3. NOTE: This step is replaced in section B.3.6.1.
  4. Return true.

(ToBoolean(argument)) 1. If argument is a Boolean, return argument.

argumentBoolean ならばそのまま argument を返します。この Boolean は ECMAScript Language Type (後の Internal Method, Internal Slot で説明をします) の1つで、 true または false のどちらかの値を持つおなじみの型です。

(ToBoolean(argument)) 2. If argument is one of undefined, null, +0𝔽, -0𝔽, NaN, 0, or the empty String, return false.

argument が上のいずれかの値であれば false を返します。 undefinednullNaN、 空文字はそのままですが、 +0𝔽-0𝔽0 は見慣れない表現です。これらは 5.2.5 Mathematical Operations に説明があり、下付きの 𝔽 は その値が Number 型であることを、下付きの ℤ は BigInt 型であることを表すときに使う表現です。

(ToBoolean(argument)) 3. NOTE: This step is replaced in section B.3.6.1.

書いてあるとおり B.3.6.1 Changes to ToBoolean を見ると、以下のように記載されています。

The following step replaces step 3 of ToBoolean:

  1. If argument is an Object and argument has an [[IsHTMLDDA]] internal slot, return false.

この [[IsHTMLDDA]] というものは何でしょうか。これについてはかなりトリビアルな話題になりますが、説明はリンク先のすぐ上の B.3.6 The [[IsHTMLDDA]] Internal Slot に書いてあります。引用をすると、

An [[IsHTMLDDA]] internal slot may exist on host-defined objects. Objects with an [[IsHTMLDDA]] internal slot behave like undefined in the ToBoolean and IsLooselyEqual abstract operations and when used as an operand for the typeof operator.

とあります。この host-defined objects というのは後で解説しますが、 [[IsHTMLDDA]] (という属性のようなもの)を持つオブジェクトは特定の抽象操作や演算子において undefined のように振る舞うということを言っています。

Web ブラウザの document.all[[IsHTMLDDA]] を持つオブジェクトなのですが、以下のように奇妙な振る舞いをします。

Boolean(document.all);     // false
document.all == undefined; // true
document.all == null;      // true
typeof document.all;       // undefined

この document.all がどういうものなのかは JavaScriptで密かに誤解されていること5選 を見ていただければわかるかと思います。

Hosts and Implementations について

後で解説すると書いた host-defined objects について書きます。仕様書では 4.2 Hosts and Implementations に説明があります。先に host について説明すると、これは D Host Layering Points に列挙されている機能を定義する外部ソースのことであり、 WHATWG HTML などの外部仕様のことを指します。host-defined objects についての記述を引用すると、

A host-defined facility is one that defers its definition to an external source without further qualification and is listed in Annex D. Implementations that are not hosts may also provide definitions for host-defined facilities.

とあるので、 host-defined object はその定義を外部ソースに委譲しているオブジェクトのことになります。上の document.all についてもその仕様は HTML Living Standard の #dom-document-all にあります。

(ToBoolean(argument)) 4. Return true.

ToBoolean(argument) のアルゴリズムに戻ります。といっても true を返すだけです。

ToBoolean(argument) 全体としては、引数が undefinednull などのいくつかの値のときだけは false を、それ以外のときは true を返す操作であると分かりました。 Boolean(value) ではこの返り値を b という変数に割り当てているので、 btruefalse の値を持つ、ということになります。

(Boolean(value)) 2. If NewTarget is undefined, return b.

Boolean(value) のアルゴリズムの2行目にいきます。 NewTarget という今まで出てきていない何者かが undefined という値ならば b を返す、と書いてあります。ここだけではよく分からないので NewTarget を調べてみます。

NewTarget

NewTarget は仕様上メタプロパティと呼ばれるもので、その評価のアルゴリズムは 13.3.12.1 Runtime Semantics: Evaluation に書いてあります。

NewTarget : new . target

  1. Return GetNewTarget().

文法については本記事では説明しませんが、上の1行目は NewTarget をコード上で表現するときは new.target と書く、と考えれば良いです。

さて、順番に行くと GetNewTarget() について調べていくのですが、先に NewTarget の概要を掴むために MDNの説明 を見てみることにします。

The new.target meta-property lets you detect whether a function or constructor was called using the new operator. In constructors and functions invoked using the new operator, new.target returns a reference to the constructor or function that new was called upon. In normal function calls, new.target is undefined.

new 演算子を使って実行されたコンストラクタ([[Construct]] 内部メソッドを持つオブジェクト。後述します)内では new.target の値はそのコンストラクタへの参照を返し、通常の関数呼び出し内では undefined を返す、ということです。イメージが掴みやすいと思うので参照先の MDN のページにある JavaScript での実行例を引用します。

function Foo() {
  if (!new.target) {
    throw new Error("Foo() must be called with new");
  }
  console.log("Foo instantiated with new");
}

new Foo(); // Logs "Foo instantiated with new"
Foo(); // Throws "Foo() must be called with new"

上のコードを見ると、 Foo という関数オブジェクトを new 演算子付きで呼び出すと new.targetFoo への参照を返すので if の条件は false になり、 new 演算子なしで呼び出すと new.targetundefined を返すので if の条件は true になる、という動きがわかるかと思います (否定演算子があるので注意してください)。

Internal Method, Internal Slot

先ほどコンストラクタを [[Construct]] 内部メソッドを持つオブジェクトだと書きましたが、その部分について説明します。ECMAScript の仕様書では [[ ]] のように角括弧2つで文字列を囲う表現がよく出てくるのですが、それが表す意味は以下のパターンがあります。

  • ECMAScript Language Type の1つである Object 型の internal method を表す
  • ECMAScript Language Type の1つである Object 型の internal slot を表す
  • ECMAScript Specification Type の1つである Record のフィールドを表す

語彙について確認します。まず ECMAScript Language Type についてですが、これは以下のいずれかの型です。

  • Undefined
  • Null
  • Boolean
  • String
  • Symbol
  • Number
  • BigInt
  • Object

どれも馴染みのある型じゃないでしょうか。ちなみに ECMAScript language value という概念もありますが、これは上記の型で特徴づけられる具体的な値の集合で、たとえば Boolean Type の value は truefalse の2つのみになります。

Object 型の internal method (内部メソッド) と internal slot (内部スロット) ですが、これは仕様書内の説明のために用いられる概念で、オブジェクトの振る舞いを定義するために用いられます (仕様書ではこの辺りに説明があります)。たとえば、 [[Get]] という内部メソッドはオブジェクトのプロパティを取得するために、 [[PrivateMethods]] という内部スロットは (オブジェクトがクラスのときに) static でないメソッドとそのクラスのアクセサを表すリストを表すために用いられます。

次に ECMAScript Specification Type を確認します。こちらは先ほどの internal method / internal slot と同様、仕様書内で様々な動作を定義するために使われる型で、必ずしも実装上に現れるものではありません。

Record はその ECMAScript Specification Type の1つで、仕様書内で使われる辞書型のようなものです。その辞書のキーに当たるものがフィールドと呼ばれており、 [[ ]] で文字列を囲って表現されるというわけです。

さて、 GetNewTarget() に戻ります。仕様は 9.4.5 GetNewTarget() にあります。アルゴリズムのステップだけ引用すると、

  1. Let envRec be GetThisEnvironment().
  2. Assert: envRec has a [[NewTarget]] field.
  3. Return envRec.[[NewTarget]].

となっています。これも各ステップについて見ていきます。

(GetNewTarget()) 1. Let envRec be GetThisEnvironment().

envRecGetThisEnvironment() の返り値を割り当てます。

また新たな抽象操作の GetThisEnvironment() が出てきたので、そのアルゴリズムを確認します。

The abstract operation GetThisEnvironment takes no arguments and returns an Environment Record. It finds the Environment Record that currently supplies the binding of the keyword this. It performs the following steps when called:

  1. Let env be the running execution context's LexicalEnvironment.
  2. Repeat,
    1. Let exists be env.HasThisBinding().
    2. If exists is true, return env.
    3. Let outer be env.[[OuterEnv]].
    4. Assert: outer is not null.
    5. Set env to outer.

(※ 順序付きリストの表現上、アルゴリズムの各行頭の番号を引用元と変えています)

Environment Record

アルゴリズムの各ステップへ入る前に、 "The abstract operation GetThisEnvironment takes no arguments and returns an Environment Record." の文にある Environment Record を確認していきましょう。

Environment Record は ECMAScript specification types の1つで、ざっくりいうと特定のスコープにある変数や関数の識別子をまとめてその外側にある Environment Record を指すポインタを持つことでスコープのネストを表現するものです。

Environment Record は抽象クラスのような存在で、以下の ECMAScript specification types に継承されています。とくに Function Environment Record と Module Environment Record は さらに Declarative Environment Record を継承しています。

  • Declarative Environment Record
    • Function Environment Record
    • Module Environment Record
  • Object Environment Record
  • Global Environment Record

たとえば、以下のネストした function foo()bar() はそれぞれ独自の Function Environment Record (A)、 (B) を持ちます。

function foo() { // (A)
  function bar() { // (B)
    let y = 20;
    console.log(`x: ${x}, y: ${y}`); // x: 10, y: 20
  }
  let x = 10;
  console.log(`x: ${x}`); // x: 10
  bar();
}
foo();

(A) は識別子として barx を持ち、 (B) は識別子として y を持ちます。 (B) 内で x への参照ができるのは、現在の Environment Record 内で識別子が見つからない場合に外側のスコープ ([[OuterEnv]] フィールドとして持つ) を見て解決を図るからです。

要約すると、アルゴリズムのステップの前の文が言っていることは以下になります。

  1. GetThisEnvironment という抽象操作は引数を取らず、 Environment Record を返すものである。
  2. GetThisEnvironment が返す Environment Record は、現在の this のバインディングを提供するものである。

では、アルゴリズムのステップに入っていきましょう。

(GetThisEnvironment()) 1. Let env be the running execution context's LexicalEnvironment.

env に running execution context の LexicalEnvironment を割り当てると書いてありますが、ここでもそれぞれの語彙を確認していきます。

Running Execution Context

まず running execution context についてです。これは実行中の execution context のことで、 execution context は 9.4 Execution Contexts に説明があります。

An execution context is a specification device that is used to track the runtime evaluation of code by an ECMAScript implementation.

コードの実行時評価を追跡するために用いられる仕様上の道具、とありますが、今どのコードを実行しているのか、そのコードからはどのような変数や関数にアクセスできるのかなどの状況を表すための仕様上の概念と考えて良いと思います。

execution context はスタックで管理されるようになっており、新しい execution context が作成されるとそのスタックに push され、実行が終わるとスタックから pop されます。execution context を管理するスタックを execution context stack と呼び、スタックの一番上の execution context を running execution context と呼びます。

The execution context stack is used to track execution contexts. The running execution context is always the top element of this stack. A new execution context is created whenever control is transferred from the executable code associated with the currently running execution context to executable code that is not associated with that execution context. The newly created execution context is pushed onto the stack and becomes the running execution context.

execution context stack は V8 などの JavaScript エンジンにおけるコールスタックのことです。

// 以下のスクリプトを実行しているとする
function foo() {
  ...
  bar();
}
function bar() {
  ... // <- 現在この行を実行中とする
}
foo();

上のようなコードがあり、コード中のコメントのように bar() の実行をしているときコールスタックは以下のようになります。なお、下図の Global Execution Context は関数の外側にあるコードを実行する際の execution context を表しています。また running execution context は、スタック内に要素が存在すれば必ず最上位の execution context になります。

Execution Context Stack
図1:Execution Context Stack の模式図

なお execution context / running execution context については、 コールスタックと実行コンテキスト|イベントループとプロミスチェーンで学ぶJavaScriptの非同期処理 がとてもわかりやすいのでそちらを見ることをお勧めします。

LexicalEnvironment

LexicalEnvironment については Table 26: Additional State Components for ECMAScript Code Execution Contexts に記述があります。まず ECMAScript code execution contexts という execution contexts が持ついくつかの構成要素に加えて以下の3つを要素に持つ、これまた仕様上の概念があるのですが、この要素の中の1つが LexicalEnvironment です。

  • LexicalEnvironment
  • VariableEnvironment
  • PrivateEnrironment

それぞれが何であるのかの細かい説明は省きますが、大雑把に言うと LexicalEnvironment はそのコードが存在する (ブロックスコープや catch 節などの) スコープのことを指します。これは LexicalEnvironment の説明として

Identifies the Environment Record used to resolve identifier references made by code within this execution context.

と書かれており、 Environment Record を表していることから想像できます。

LexicalEnvironment は (ECMAScript code) execution context に紐づく概念なので、 execution context がスタックで管理されることでどのような実行順で動きどのようなスコープになっているのかを LexicalEnvironment が管理できると筆者は捉えています。

色々と難しい概念が登場しましたが、問題にしていた "Let env be the running execution context's LexicalEnvironment." という文は、実行中の execution context (実行しているコードとその状態) に紐づく LexicalEnvironment (変数や関数のスコープ) を env に割り当てる、という意味に解釈できます。

(GetThisEnvironment()) 2.1. Let exists be env.HasThisBinding().

env は LexicalEnvironment で、 LexicalEnvironment は Environment Record なのでそのメソッドを確認します。見てみると Table 16: Abstract Methods of Environment Records に書いてあることが分かります。この HasThisBinding() の説明を引用します。

Determine if an Environment Record establishes a this binding. Return true if it does and false if it does not. (強調引用者)

つまりこの行は envthis バインディングを持つかどうかを true or false で返し、それを exists という変数に割り当てるということになります。

(GetThisEnvironment()) 2.2. If exists is true, return env.

envthis バインディングを持つならば env を返します。

(GetThisEnvironment()) 2.3. Let outer be env.[[OuterEnv]].

env.[[OuterEnv]]outer という変数に割り当てます。Environment Record の箇所でも出てきましたが、 [[OuterEnv]] は Environment Record のフィールドで、その Environment Record が外側のスコープを参照するためのものです。

(GetThisEnvironment()) 2.4. Assert: outer is not null.

outernull ではないことを強調しているだけで、この行では操作はしていません。

(GetThisEnvironment()) 2.5. Set env to outer.

envouter に設定します。

2.1 ~ 2.5 はループになっており、1周ごとに1つ外側の環境を見に行くように動きます。一見するとループが止まる保証がないように見えますが、 GetThisEnvironment() の NOTE にも書いてあるようにいつかは global environment に到達する (そして global environment は this バインディングを持つ) ので必ず止まります。

(GetNewTarget()) 2. Assert: envRec has a [[NewTarget]] field.

GetNewTarget() に戻ってきました。

envRec オブジェクトが [[NewTarget]] のフィールドを持っていることを強調しています。

[[NewTarget]] については、9.1.1.3 Function Environment Records の Table 18: Additional Fields of Function Environment Records に書いてあります。値は Objectundefined のいずれかで、説明を見ると [[Construct]] という内部メソッドによって Environment Record が作られたとき、 [[Construct]]newTarget のパラメータの値が [[NewTarget]] になる (そうでなければ undefined になる)、とあります。

[[Construct]] を見ると、6.1.7.2 Object Internal Methods and Internal SlotsのTable 5: Additional Essential Internal Methods of Function Objectsに説明がありますが、この表のすぐ上にある説明の方が分かりやすいと思うのでそちらを引用します。

Table 5 summarizes additional essential internal methods that are supported by objects that may be called as functions. A function object is an object that supports the [[Call]] internal method. A constructor is an object that supports the [[Construct]] internal method. Every object that supports [[Construct]] must support [[Call]]; that is, every constructor must be a function object.

つまり、 [[Call]] という内部メソッドを持つオブジェクトのことを関数といい、 [[Call]] を持ちかつ [[Construct]] を持つオブジェクトをコンストラクタと呼ぶと言っています。

(GetNewTarget()) 3. Return envRec.[[NewTarget]].

envRec.[[NewTarget]] を呼び出し元に返します。

(再訪) (Boolean(value)) 2. If NewTarget is undefined, return b.

長かったですが、 NewTarget のアルゴリズムで出た GetNewTarget() について見終わったので Boolean(value) に戻ります。

NewTarget が undefined、 つまり通常の関数として Boolean(value) を呼び出したときは b (これは ToBoolean(value) の返り値でした) を返すとあるので、ここから先のステップは new Boolean(value) のように new 演算子を使って関数を呼び出した場合の処理になりますが、本記事はここで止めておきます。

おわりに

Boolean(value) のアルゴリズムステップのうちのたった2行でしたが、詳しく見ていくとかなり多くの知識が必要になることが感じられたのではないでしょうか。この記事だけでは ECMAScript の仕様全体を理解するための情報としては色々と不足していますが、 JavaScript のコードを書いていて仕様を確認したいけどハードルが高く読む気にならない、という状態から脱却する助けに少しでもなれたらいいなと思っております。

上の本文中では言及しなかったものの、本記事を書くにあたって参考にした文献のリンクを載せます。

最後になりますが、 Opt Technologies ではエンジニアを募集中です。カジュアル面談も可能ですので、下記リンク先よりお気軽にご応募ください。