ES6 classes: deferring the creation step

Claude Pache claude.pache at
Thu Jul 3 14:25:11 PDT 2014

Here is an updated version of my proposal. Superficially, there are notable
changes on how the things are presented, but the observable behaviour remains
basically the same.

For a quick understanding, the Simple Description should suffice.
In the Detailed Semantics, I’ve tried to give just enough details
to make the precise semantics clear.

Simple Description

If a constructor `C` does not use `super`, it is assumed to not be a subclass,
and it has the ES5- behaviour (or, equivalently, the current specced behaviour
without the user-overridable-and-callable @@create hook),
except that the [[Prototype]] may not always be taken from `C.prototype`,
but from some `D.prototype`, where `D` is a subclass of `C` as described below.

If a constructor `C` uses `super` in its code, let’s say:

    class C extends B {
        constructor(...args) {
            /* 1: preliminary code that doesn't contain calls to a super-method */
            // ...
            /* 2: call to a super-constructor */
            /* 3: the rest of the code */
            // ...
the following occurs when `C` is invoked as constructor, 
e.g. using `new C(...args)`:

1.  During phase /* 1 */, the this-binding is uninitialised; trying to access 
    it through an explicit `this` keyword will throw a ReferenceError.

2.  At phase /* 2 */, a call to the super-constructor (or, indeed, any
    super-method call) will in fact invoke it with the semantics of a
    constructor, and will initialise the this-binding. It is somewhat 
    as if doing,

        this = new super(...whatever)

    except that the default prototype of the created object will be
    `D.prototype` rather than `super.prototype`, where `D` is the constructor
    on which the `new` operator was originally applied (the reference to `D`
    being forwarded by the super-call as needed).
3.  During phase /* 3 */, the this-binding is initialised, and any call to a 
    super-method has the normal semantics of method (not constructor).

That’s it.

The fact that the `super` is called with the semantics of a constructor means
that subclassing will just work, including for functions that have different
behaviour when used as constructor and when used as function or method.
This is the case, e.g., for bound functions (see [1]), or, say, `Date` 
(that is, without the need of some hack).

Note that `constructor(...args) { super(...args) }` has now the same meaning as
`constructor(...args) { return super(...args) }` (when called as constructor), 
so that the issue mentioned in [bug 2491] needs to be reconsidered.
This is resolved by tweaking `Object.[[Construct]]`, as shown at the
end of that message.

The @@create hook is gone, in order to avoid completely the observability
of partially-constructed built-in objects.

Also, there is no @@new hook [2], because the `constructor` method *is*
the @@new hook. A notable fact is that a constructor can always override its
default constructed object by  returning another object, which is a legacy 
feature of ES5- non-subclassable constructors.

(And, in order to give credit where it is due, I must mention that it is the 
@@new proposal of Jason [2] which has been the starting point of my reflection,
by trying to give the most rational semantics of the @@new behaviour.)

Detailed semantics

Additional semantics for [[Call]] internal method

Recall that the call behaviour of a function is encoded in the [[Call]]
internal method. Besides its current behaviour, the following features
are added. 

The signature has a supplementary optional argument `thisConstructor`:

    F.[[Call]] (thisArgument, argumentsList, thisConstructor)

The intent of `thisConstructor` is to keep track of the original constructor
on which a `new` operator was applied.

The `thisArgument` may receive the special value `empty`, meaning that the
this-binding will not be initialised; it may be initialised once
(as for a `const`-binding). 

Moreover, the Completion record (either normal or abrupt) returned
from [[Call]] will hold an additional [[thisValue]] field, 
which, unless otherwise specified:

* is set to the original `thisArgument` if it was not empty; or,
* is set to the value (at the time of completion) of the this-binding, 
  if it was initially uninitialised but has been initialised; or,
* is absent if the this-binding was left uninitialised.

(If an abrupt completion is forwarded, its [[thisValue]] field
shall be modified as needed.)

We will use the following notations (they are properly methods of
function environment records):

* GetThisBinding() ― get the value of the this-binding of the appropriate 
  function environment record.
* InitializeThisBinding(`value`) ― initialise an uninitialised this-binding.
* GetThisConstructor() — retrieve the `thisConstructor` value that was
  passed to [[Call]].

Modified semantics of the [[Construct]] internal method

The [[Construct]] internal method determines the behaviour of a function
when invoked as constructor. Relatively to what is currently specced, it has
a supplementary argument `receiver`:

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

The argument `receiver` is intended to receive the reference to the original
constructor on which a `new` operator was applied.
In particular, `new F(...args)` will trigger `F.[[Construct]](F, args)`.

Main differences from the currently specified [[Construct]] internal method are:

* when effectively constructing the object, the prototype is searched on
  `receiver.prototype` rather than `F.prototype`;
* the Completion record returned by [[Construct]], if not abrupt, shall have 
  its [[value]] field and its [[thisValue]] field set to a same value of
  type Object. (That condition is here for easing the subsequent use of
  the Completion Record.)
Also, it will be handy to use the following notation, where `R` is a
Completion record typically returned from [[Call]]:

as an abbreviation for:

1.  If `R` is an abrupt completion, return `R`.
2.  Else, if Type(`R.[[value]]`) is Object, return
    `Completion{[[type]]: normal, [[value]]: R.[[value]], [[thisValue]]: R.[[value]]}`.
3.  Else, if `R.[[thisValue]]` is present and Type(`R.[[thisValue]]`) is Object, return
    `Completion{[[type]]: normal, [[value]]: R.[[thisValue]], [[thisValue]]: R.[[thisValue]]}`.
4.  Else, throw a ReferenceError, with its `[[thisValue]]` field set to
    `R.[[thisValue]]` if `R.[[thisValue]]` is present.

F.[[Construct]] (receiver, argumentsList) for user-defined functions

When `F` is a user-defined, non-arrow function its [[Construct]] internal
method does the following. 

* If `F` does not use `super` (that is, in spec language, if `F.[[NeedsSuper]]` is false):
    1.  Let `obj` be the result of OrdinaryCreateFromConstructor(`receiver.prototype`, "%ObjectPrototype").  
        // this is roughly `obj = Object.create(receiver.prototype || Object.prototype)`
    2.  ReturnIfAbrupt(`obj`).
    3.  Let `result` be the result of `F.[[Call]](obj, argumentsList, receiver)`.
    4.  ReturnNormalizedConstructCompletion(`result`).

* If `F` uses `super`, the creation step is deferred:
    1. Let `result` be the result of `F.[[Call]](empty, argumentsList, receiver)`.
    2. ReturnNormalizedConstructCompletion(`result`).

Semantics of a super-method call

When, say, `super.method(..args)` is called, the following steps are taken:

1. Let `F` be the method referenced by `super.method`.
2. Let `thisValue = GetThisBinding()`.
3. If `thisValue` is not empty,  
    a. Let `result` be the result of `F.[[Call]](thisValue, args)`.
4. Else,  
    a. Let `receiver = GetThisConstructor()`.  
    b. Let `result` be the result of `F.[[Construct]](receiver, args)`.  
    c. If Type(`result.[[thisValue]]`) is Object,  
        i. InitializeThisBinding(`result.[[value]]`).  
    d. Assert: If the test in previous step was negative,
       then `result` is an abrupt completion.
5. Return `result`.

Special behaviour of the Object constructor

The [[Construct]] internal method of `Object`:

    Object.[[Construct]] (receiver, argumentsList)

has the following semantics:

1. If `receiver` is `Object`,  
    a. Let `result` be `Object.[[Call](undefined, argumentsList)`.  
        // this is the usual factory function.
2. Else,  
    a. Let `result` be OrdinaryCreateFromConstructor(`receiver`, "%ObjectPrototype").  
       // which is roughly `Object.create(receiver.prototype || Object.prototype)`
3. ReturnNormalizedConstructCompletion(`result`).

The test of step 1 distinguishes between invocations of [[Construct]]
coming directly from `new` from those triggered by `super`.
That will gracefully handle accidental calls of the Object constructor
that occur in situations described in [bug 2491].

[bug 2491]:

More information about the es-discuss mailing list