仕様書から読み解くECMAScriptのnew

September 19th, 2020

ECMAScriptのnew演算子をECMAScript2020の仕様書から読み解く話。

確認することは以下の2点である。

  • newが変数名として使えない理由
  • newを使ったオブジェクトの生成はどのように働いているか
var new; // Syntax Error
let new; // Syntax Error
var new = 1; // Syntax Error
let new = 1; // Syntax Error
const new = 1; // Syntax Error

var obj1 = { new: 1 }; // ok
let obj2 = {};
obj2.new = 1; // ok
obj2.new // ok
obj2["new"] // ok
function Vehicle(plate, capacity) {
  this.plate = plate;
  this.capacity = capacity;
}

let car = new Vehicle("A-1234", 4);

classを使っていない古い書き方だが、実際のコードと仕様書を照らし合わせていく関係上こちらの表記のほうが分かりやすいのでclassを使わずに進める。

また、ES6でnew.targetも出てきたが今回これは対象外とする。

// new.target
function User (name) {
  this.name = name;
  if (!new.target) {
    throw new Error(`Object 'User' must have a 'new' prefix.`);
  }
}

let Alice = new User("Alice"); // ok
let Bob = User("Bob"); // Uncaught Error

表記編

newは予約語なので変数名には使えない」と技術書に書かれていたりする。まずはこれを確かめる。

事前知識(表記編)

ECMAScriptで書かれたソースコードのテキストはUnicodeのコードポイントとして扱われた後、

  • トークン(token)
  • 行終端(line termination)
  • コメント(comments)
  • 空白(white space)

の4つで構成された列に変換される。これら4つはinput elementと呼ばれ、テキストを左から右に読み取りながら次のinput elementが続く限り、できるだけ長い列に変換される。

この変換にはlexical grammarという文法が使われる。ECMAScriptの仕様書にはいくつかの文法が定められているがlexical grammarはその1つ。lexical grammarはContext-Free Grammarで表記されていて左辺::右辺の形式を取る。例えばInputElementDivは以下のように表す。

InputElementDiv::
  WhiteSpace
  LineTerminator
  Comment
  CommonToken
  DivPunctuator
  RightBracePunctuator

これは左辺InputElementDiv

  • WhiteSpace
  • LineTerminator
  • Comment
  • CommonToken
  • DivPunctuator
  • RightBracePunctuator

の6種類から成ることを示している。

また、この6つはそれ自体が新たな左辺になる。このようにlexical grammarは終端まで再帰的に続いていく。

lexical grammarを使った解析の後は、tokenの列が構文的に正しいコンポーネントとなっているかを確かめるためにsyntactic grammarが適用される。syntactic grammarもlexical grammar同様、ECMAScriptに定められている文法の1つ。表記は左辺:右辺となり、lexical grammarと比べて:の数が1個少なくなっている。

例えばリテラル表記は以下のように示される。

Literal:
  NullLiteral
  BooleanLiteral
  NumericLiteral
  StringLiteral

tokenとしてのnew

さて、lexical grammarの中には識別子の名前を決めるためIdentifierNameと呼ばれるtokenが存在する。

IdentifierName

IdentifierName::
  IdentifierStart
  IdentifierNameIdentifierPart

さらにIdentifierNameのうち、ifwhileasyncawaitのように文法的に意味があるものはkeywordと呼ばれる。そして多くのkeywordはreserved wordにも分類される

上記のことはKeywords and Reserved Wordsで確認できる。

A keyword is a token that matches IdentifierName, but also has a syntactic use; that is, it appears literally, in a fixed width font, in some syntactic production. The keywords of ECMAScript include if, while, async, await, and many others.

A reserved word is an IdentifierName that cannot be used as an identifier. Many keywords are reserved words, but some are not, and some are reserved only in certain contexts. if and while are reserved words. await is reserved only inside async functions and modules. async is not reserved; it can be used as a variable name or statement label without restriction.

以上のことを(厳密ではないが)図にすると以下のようになる。

ALT

newは上の図のうち赤色の部分、つまりreserved wordに属する。lexical grammarの中でreserved wordはReservedWordとされ以下のように表される。

ReservedWord

ReservedWord::one of
  await break case catch class const continue debugger default
    delete do else enum export extends false finally for function
    if import in instanceof new null return super switch this throw
    true try typeof var void while with yield

さらに仕様書には以下のように書かれている。

A reserved word is an IdentifierName that cannot be used as an identifier.

予約語は識別子として使うことのできないIdentifierNameのことである。(拙訳)

ここでいうidentifier(識別子)はsyntactic grammarで登場する。

変数にnewが指定できない理由

変数名にnewが使えない理由を見ていきたい。syntactic grammarの中ではvarを使った変数宣言にVariableStatementVariableDeclarationListVariableDeclarationが使われる。

VariableStatement

VariableStatement[Yield, Await]:
  varVariableDeclarationList[+In, ?Yield, ?Await] ;

VariableDeclarationList[In, Yield, Await]:
  VariableDeclaration[?In, ?Yield, ?Await]
  VariableDeclarationList[?In, ?Yield, ?Await] , VariableDeclaration[?In, ?Yield, ?Await]

VariableDeclaration[In, Yield, Await]:
  BindingIdentifier[?Yield, ?Await] Initializer[?In, ?Yield, ?Await]opt
  BindingPattern[?Yield, ?Await] Initializer[?In, ?Yield, ?Await]

一方、letconstを使った変数宣言にはLexicalDeclarationLetOrConstBindingListLexicalBindingが現れる。

Let and Const Declarations

LexicalDeclaration[In, Yield, Await]:
  LetOrConst BindingList[?In, ?Yield, ?Await];

LetOrConst:
  let
  const

BindingList[In, Yield, Await]:
  LexicalBinding[?In, ?Yield, ?Await]
  BindingList[?In, ?Yield, ?Await] , LexicalBinding[?In, ?Yield, ?Await]

LexicalBinding[In, Yield, Await]:
  BindingIdentifier[?Yield, ?Await] Initializer[?In, ?Yield, ?Await] opt
  BindingPattern[?Yield, ?Await] Initializer[?In, ?Yield, ?Await]

[]内に登場するIn?Yield、末尾にあるoptについては今回触れない。

上記を見比べてみるとvarを使った変数宣言、letまたはconstを使った変数宣言には共通して内部でBindingIdentifierが使われている。

さらにこのBindingIdentifierを見てみると内部でIdentifierが登場する。

BindingIdentifier

BindingIdentifier[Yield, Await]:
  Identifier
  yield
  await

Identifier

Identifier:
  IdentifierName but not ReservedWord

つまり、IdentifierとなれるのはIdentifierNameのうちReservedWordに属さないものである。以上のことからnewIdentifierとして使うことができないことが分かる。

var new; // Syntax Error
let new; // Syntax Error
var new = 1; // Syntax Error
let new = 1; // Syntax Error
const new = 1; // Syntax Error

一方、オブジェクトをリテラルで表記したり、プロパティにアクセスする際のsyntactic grammarにIdentifierは登場しない。 そのため以下のコードはエラーにならない。

var obj1 = { new: 1 }; // ok
let obj2 = {};
obj2.new = 1; // ok
obj2.new // ok
obj2["new"] // ok

tokenとしてのnewが許される表記

syntactic grammarでnewの登場が許されるのものの一つにMemberExpressionがある。

MemberExpression

MemberExpression[Yield, Await]:
  PrimaryExpression[?Yield, ?Await]
  MemberExpression[?Yield, ?Await] [ Expression[+In, ?Yield, ?Await] ]
  MemberExpression[?Yield, ?Await] . IdentifierName
  MemberExpression[?Yield, ?Await] TemplateLiteral[?Yield, ?Await, +Tagged]
  SuperProperty[?Yield, ?Await]
  MetaProperty
  new MemberExpression[?Yield, ?Await] Arguments[?Yield, ?Await]

一番下にnew MemberExpression[?Yield, ?Await] Arguments[?Yield, ?Await]という表記がある。これは冒頭に掲げたコードが該当する。

let car = new Vehicle("A-1234", 4);
// '=' より右側の表記が 'new MemberExpression[?Yield, ?Await] Arguments[?Yield, ?Await]' に当てはまる

そして、これまでのsyntactic grammarとコードの関係を図に表すと以下のようになる(が、本来コードへたどり着くにはもう少しsyntactic grammarをたどる必要がある。例えばVehicleはそれ自身がMemberExpressionであるが、途中IdentifierReferenceにたどり着く)。

tree

ではMemberExpressionのうち、new MemberExpression[?Yield, ?Await] Arguments[?Yield, ?Await]は実行時にどのような働きをしているのだろうか。

ランタイム編

new MemberExpression[?Yield, ?Await] Arguments[?Yield, ?Await]の実行時の動きを見る前に仕様書におけるオブジェクトの振る舞いを見ておきたい。

事前知識(ランタイム編)

ECMAScriptにおけるオブジェクトの振る舞いを記述するために、仕様書では特定のアルゴリズムを記述したinternal methodと、オブジェクトの状態を記述したinternal slotがある。両方とも仕様書の中でのみ扱われる。

internal slotとinternal methodを使った表記はECMAScriptのコードでオブジェクトを使ったときの書き方に似ている。

例えばあるオブジェクトOのinternal slot[[Slot]]を参照するときは以下のようになる。

O.[[Slot]]

また、Oのinternal method[[Method]]を、引数argumentと一緒に呼ぶ場合は以下のようになる。

O.[[Method]](argument)

internal slotとinternal methodを取り上げた理由は、ECMAScriptのオブジェクトには共通して複数のinternal methodが実装されてなければならないと仕様書の中で定められているからだ。ECMAScriptのオブジェクトは、このベースとなるinternal methodに加えて追加のinternal slotやinternal methodを実装することで様々なオブジェクトを表現している。

今回注目しているnewに関連するオブジェクトのうち、重要なものは以下。

  • function object ... 必要なinternal methodに加え、追加で[[Call]]というinternal methodが定義されている。
  • constructor ... function objectに更に追加で[[Construct]]というinternal methodが定義されている。

関係図を示すと以下の継承のような形になる。

ALT

[[Call]]は関数呼び出しの際に用いられる一方、[[Construct]]はオブジェクトの生成に用いられる。このため以降は[[Construct]]に焦点を当てる。

[[Construct]]について

[[Construct]]の説明は以下のように書かれている。

Table 7: Additional Essential Internal Methods of Function Objects

Creates an object. Invoked via the new operator or a super call. The first argument to the internal method is a list containing the arguments of the constructor invocation or the super call. The second argument is the object to which the new operator was initially applied. Objects that implement this internal method are called constructors. A function object is not necessarily a constructor and such non-constructor function objects do not have a [[Construct]] internal method.

オブジェクトを生成し、new演算子かsuperの宣言を経由して呼び出される。第一引数にconstructorの呼び出しの引数を含むリスト、またはsuperを使った宣言の引数を含むリストを与える。第二引数にはnew演算子が最初に適用されたオブジェクトを与える。このinternal methodを実装したオブジェクトはconstructorと呼ばれる。function objectはconstructorである必要はなく、constructorではないfunction objectは[[Construct]]というinternal methodを持たない。(拙訳)

以上から分かることは

  • newsuperを使うことで新しいオブジェクトが生成できる
  • [[Construct]]を実装したオブジェクトはconstructorと呼ばれる

である。

function Vehicle(plate, capacity) {
  this.plate = plate;
  this.capacity = capacity;
}

// new演算子を使うことで新しいオブジェクトを生成している
let car = new Vehicle("A-1234", 4);

ここから[[Construct]]が具体的に使われている場面を見ていきたい。

Runtime Semantics

ランタイム時に呼び出される意味論のことはruntime semanticsと呼ばれる。runtime semanticsでは仕様書の中で定義されたabstract operationと呼ばれるアルゴリズムを用いて疑似コードのような形で振る舞いが表現される。

余談だが、internal methodとabstract operationは異なる。前者はオブジェクトにおけるメソッドのような位置付けに対して、後者は仕様書内で使われるアルゴリズムを簡潔に表現するために書かれたものである。 表記も前者なら[[Notation]]の形で表現されるが後者はNotationと表現される。

話を戻し、先程のsyntactic grammarを見る。

new MemberExpression[?Yield, ?Await] Arguments[?Yield, ?Await]

このsyntactic grammarには以下のruntime semanticsが定義されている。

12.3.5.1 Runtime Semantics: Evaluation

12.3.5.1 Runtime Semantics: Evaluation

MemberExpression : new MemberExpression Arguments
  1. Return ? EvaluateNew(MemberExpression, Arguments).

実行されるのはReturn ? EvaluateNew(MemberExpression, Arguments)になる。

ここに見知らぬReturn?EvaluateNewという3つのabstract operationが登場する。

Returnは実際のECMAScriptのコードで扱うreturnとほぼ同じである。?は実行中に何かしらのエラーが出た時に即Returnして中断するためのabstract operationである。残りのEvaluateNewに着目する。

12.3.5.1.1 Runtime Semantics: EvaluateNew ( constructExpr , arguments )

12.3.5.1.1 Runtime Semantics: EvaluateNew ( constructExpr , arguments )

The abstract operation EvaluateNew with arguments constructExpr,
and arguments performs the following steps:

1. Assert: constructExpr is either a NewExpression or a MemberExpression.
2. Assert: arguments is either empty or an Arguments.
3. Let ref be the result of evaluating constructExpr.
4. Let constructor be ? GetValue(ref).
5. If arguments is empty, let argList be a new empty List.
6. Else,
  a. Let argList be ? ArgumentListEvaluation of arguments.
7. If IsConstructor(constructor) is false, throw a TypeError exception.
8. Return ? Construct(constructor, argList).

abstract operationの呼び出しが続いていくが、今度は8のReturn ? Construct(constructor, argList)のうち、Construct(constructor, argList)に着目したい。

7.3.14 Construct ( F [ , argumentsList [ , newTarget ] ] )

7.3.14 Construct (F [ , argumentsList [ , newTarget ] ])

The abstract operation Construct is used to call the [[Construct]] internal method of a function object. 
The operation is called with arguments F, and optionally argumentsList, and newTarget where F is the function object.
argumentsList and newTarget are the values to be passed as the corresponding arguments of the internal method.
If argumentsList is not present, a new empty List is used as its value.
If newTarget is not present, F is used as its value. This abstract operation performs the following steps:

1. If newTarget is not present, set newTarget to F.
2. If argumentsList is not present, set argumentsList to a new empty List.
3. Assert: IsConstructor(F) is true.
4. Assert: IsConstructor(newTarget) is true.
5. Return ? F.[[Construct]](argumentsList, newTarget).

5でようやく[[Construct]]が記述されているのが分かる。Fというオブジェクトに定義されたinternal method[[Construct]]を2つの引数(argumentsList, newTarget)で呼び出してその戻り値を取得している。

まとめると、new MemberExpression Argumentsの形で表記されたECMAScriptはランタイム時、EvaluateNewConstructといったabstract operationを経由して、最終的にはF.[[Construct]]というinternal methodを呼び出して新たにインスタンスとしてのオブジェクトを生成する。

ところでFというオブジェクトが[[Construct]]というinternal methodを持っているということは、Fはconstructorであり、function objectでもある

ではこのFはどこから来たのだろうか。

constructorの取得

ここで一度、これまで登場したRuntime Semanticsを一部表記を省略して並べ、abstract operationの引数と変数の関係に注目したい。

MemberExpression: new MemberExpression Arguments
  1. Return ? EvaluateNew(MemberExpression, Arguments).


EvaluateNew( constructExpr , arguments )
  ...
  3. Let ref be the result of evaluating constructExpr.
  4. Let constructor be ? GetValue(ref).
  5. If arguments is empty, let argList be a new empty List.
  6. Else,
    a. Let argList be ? ArgumentListEvaluation of arguments.
  7. If IsConstructor(constructor) is false, throw a TypeError exception.
  8. Return ? Construct(constructor, argList).


Construct ( F [ , argumentsList [ , newTarget ] ] )
  1. If newTarget is not present, set newTarget to F.
  2. If argumentsList is not present, set argumentsList to a new empty List.
  ...
  5. Return ? F.[[Construct]](argumentsList, newTarget).


F.[[Construct]](argumentsList, newTarget)

Fが初めて登場するのはConstructの第一引数としてである。

そのConstructが呼び出されるEvaluateNewでは、4で変数constructorGetValue(ref)というabstract operationの結果として格納されている。そしてこれがConstructの第一引数となっている。

runtime 01

続いて、同じくEvaluateNew内で呼び出されているabstract operationのGetValue(ref)に注目する。引数refは3にあるとおり、constructExprの評価の結果とされている。

そしてこのconstructExprEvaluateNew自身の第一引数であり、MemberExpressionでもある。

runtime 02

さらに今一度、表記編で扱った画像を確認したい。

tree

以上からrefVehicleの評価の結果、つまりfunctionで宣言したVehicleの参照の結果を意味する。言い換えると、EvaluateNew内で生成される変数constructorの源はVehicleにある。

// 宣言時にconstructorになっている…?
function Vehicle(plate, capacity) {
  this.plate = plate;
  this.capacity = capacity;
}

let car = new Vehicle("A-1234", 4);

constructorの生成

順番としては前後するが、function宣言時、どういったruntime semanticsが定義されているのかを見ていきたい。

表記の観点から言えば、上記のような宣言はlexical grammarにおけるFunctionDeclarationになる。

そしてFunctionDeclarationのruntime semanticsは以下の通りである。

14.1.23 Runtime Semantics: InstantiateFunctionObject

With parameter scope.

FunctionDeclaration : function BindingIdentifier ( FormalParameters ) { FunctionBody }
  1. Let name be StringValue of BindingIdentifier.
  2. Let F be OrdinaryFunctionCreate(%Function.prototype%, FormalParameters, FunctionBody, non-lexical-this, scope).
  3. Perform MakeConstructor(F).
  4. Perform SetFunctionName(F, name).
  5. Set F.[[SourceText]] to the source text matched by FunctionDeclaration.
  6. Return F.

重要な部分は以下の2点である。

  • 2でfunction objectを生成する(つまり、オブジェクトとして必要なinternal methodを追加して、さらに[[Call]]を実装している)
  • 3でconstructorを生成する(つまり、2で作ったfunction objectにさらに[[Construct]]を実装している)

このようにfunction宣言で定義された関数はfunction objectでありconstructorでもある。そしてnewを使ってインスタンスとして新たにオブジェクトを作るときはこのconstructorが参照される。

まとめ

newについてまずは表記的な観点から見た。変数名として使えないのはnewReservedWordしてlexical grammarで定義されており、syntactic grammarで使うことが許されていないためであった。

次にランタイム時のオブジェクト生成に焦点を当て、仕様書内で定義されたinternal methodの[[Construct]]に着目した。[[Construct]]はconstructorと呼ばれるオブジェクトに定義されている新しいオブジェクトを生成するinternal methodだった。そのconstructorの正体はあらかじめfunction宣言で定義された関数だった。