ECMAScript の Boolean(value)
の仕様の前半部分をなるべく詳細に読んでみました。
- はじめに
- Boolean(value) の仕様を(アルゴリズムの2ステップ目まで)読んでいく
- (Boolean(value)) 1. Let b be ToBoolean(value)
- 抽象操作
- ToBoolean(argument)
- (ToBoolean(argument)) 1. If argument is a Boolean, return argument.
- (ToBoolean(argument)) 2. If argument is one of undefined, null, +0𝔽, -0𝔽, NaN, 0ℤ, or the empty String, return false.
- (ToBoolean(argument)) 3. NOTE: This step is replaced in section B.3.6.1.
- Hosts and Implementations について
- (ToBoolean(argument)) 4. Return true.
- (Boolean(value)) 2. If NewTarget is undefined, return b.
- NewTarget
- Internal Method, Internal Slot
- (GetNewTarget()) 1. Let envRec be GetThisEnvironment().
- Environment Record
- (GetThisEnvironment()) 1. Let env be the running execution context's LexicalEnvironment.
- Running Execution Context
- LexicalEnvironment
- (GetThisEnvironment()) 2.1. Let exists be env.HasThisBinding().
- (GetThisEnvironment()) 2.2. If exists is true, return env.
- (GetThisEnvironment()) 2.3. Let outer be env.[[OuterEnv]].
- (GetThisEnvironment()) 2.4. Assert: outer is not null.
- (GetThisEnvironment()) 2.5. Set env to outer.
- (GetNewTarget()) 2. Assert: envRec has a [[NewTarget]] field.
- (GetNewTarget()) 3. Return envRec.[[NewTarget]].
- (再訪) (Boolean(value)) 2. If NewTarget is undefined, return b.
- おわりに
はじめに
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:
- Let
b
beToBoolean(value)
.- If
NewTarget
isundefined
, returnb
.- Let
O
be ?OrdinaryCreateFromConstructor(NewTarget, "%Boolean.prototype%", « [[BooleanData]] »)
.- Set
O.[[BooleanData]]
tob
.- 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.
x
を someValue
に割り当てる (JavaScrirpt のコードで表すならば let x = someValue
のような操作をする) ということです。参照なので、 x
か someValue
のどちらかの値が変更されると他方の値も変わります。また既に値が割り当てられている x
を someOtherValue
に変更するときは "Set x to someOtherValue" という表現を使います。
よって、
Let
b
beToBoolean(value)
.
という処理は b
という変数に ToBoolean(value)
という処理の返り値を割り当てるんだな、と考えれば良いです。
抽象操作
ところでこの ToBoolean(value)
ですが、仕様書では抽象操作 (Abstract Operation) と呼ばれるものになります。これは仕様書内でのみ使用される関数で、ECMAScript の実装をする JavaScript エンジンでは関数として実装される必要はないものです。別の箇所から参照しやすくするために便宜的に名前を付けた処理の塊だと思えば良いです。
ToBoolean(argument)
では、ToBoolean(argument)
という抽象操作のアルゴリズムを見ていきます。
- If
argument
is aBoolean
, returnargument
.- If
argument
is one ofundefined
,null
,+0𝔽
,-0𝔽
,NaN
,0ℤ
, or the empty String, return false.- NOTE: This step is replaced in section B.3.6.1.
- Return
true
.
(ToBoolean(argument)
) 1. If argument
is a Boolean
, return argument
.
argument
が Boolean
ならばそのまま 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
を返します。 undefined
、 null
、 NaN
、 空文字はそのままですが、 +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:
- 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)
全体としては、引数が undefined
や null
などのいくつかの値のときだけは false
を、それ以外のときは true
を返す操作であると分かりました。 Boolean(value)
ではこの返り値を b
という変数に割り当てているので、 b
は true
か false
の値を持つ、ということになります。
(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
- 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.target
は Foo
への参照を返すので if
の条件は false
になり、 new
演算子なしで呼び出すと new.target
は undefined
を返すので 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 は true
か false
の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() にあります。アルゴリズムのステップだけ引用すると、
- Let
envRec
beGetThisEnvironment()
.- Assert:
envRec
has a[[NewTarget]]
field.- Return
envRec.[[NewTarget]]
.
となっています。これも各ステップについて見ていきます。
(GetNewTarget()
) 1. Let envRec
be GetThisEnvironment()
.
envRec
に GetThisEnvironment()
の返り値を割り当てます。
また新たな抽象操作の 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:
- Let
env
be the running execution context's LexicalEnvironment.- Repeat,
- Let
exists
beenv.HasThisBinding()
.- If
exists
istrue
, returnenv
.- Let
outer
beenv.[[OuterEnv]]
.- Assert:
outer
is notnull
.- Set
env
toouter
.
(※ 順序付きリストの表現上、アルゴリズムの各行頭の番号を引用元と変えています)
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) は識別子として bar
と x
を持ち、 (B) は識別子として y
を持ちます。 (B) 内で x
への参照ができるのは、現在の Environment Record 内で識別子が見つからない場合に外側のスコープ ([[OuterEnv]]
フィールドとして持つ) を見て解決を図るからです。
要約すると、アルゴリズムのステップの前の文が言っていることは以下になります。
GetThisEnvironment
という抽象操作は引数を取らず、 Environment Record を返すものである。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 / 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. (強調引用者)
つまりこの行は env
が this
バインディングを持つかどうかを true
or false
で返し、それを exists
という変数に割り当てるということになります。
(GetThisEnvironment()
) 2.2. If exists
is true
, return env
.
env
が this
バインディングを持つならば 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
.
outer
は null
ではないことを強調しているだけで、この行では操作はしていません。
(GetThisEnvironment()
) 2.5. Set env
to outer
.
env
を outer
に設定します。
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 に書いてあります。値は Object
か undefined
のいずれかで、説明を見ると [[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 のコードを書いていて仕様を確認したいけどハードルが高く読む気にならない、という状態から脱却する助けに少しでもなれたらいいなと思っております。
上の本文中では言及しなかったものの、本記事を書くにあたって参考にした文献のリンクを載せます。
- Understanding the ECMAScript spec, part 1 · V8
- How to Read the ECMAScript Specification
- どこで読めるの?今さらきけない仕様書の在り処! | フロントエンドBlog
- Promise.prototype.then の仕様挙動|イベントループとプロミスチェーンで学ぶJavaScriptの非同期処理
- ECMAScriptの仕様/プロポーザルの調べ方を知る
- top-level awaitがどのようにES Modulesに影響するのか完全に理解する
- ECMAScript仕様輪読会
最後になりますが、 Opt Technologies ではエンジニアを募集中です。カジュアル面談も可能ですので、下記リンク先よりお気軽にご応募ください。