In the currently specced design of classes, the fact that the creation step and
the initialisation step of built-in object may be separated by arbitrary user
code is thought to be problematic.
Jason proposed a @@new behaviour in replacement of @@create that would avoid
the issue [1]. Here is a counter-proposal (or an improved proposal, ad
libidum), which is not tightly coupled to @@new. In fact, it is designed to
(well, TBH, it happens to) just work in absence of @@create or @@new hook,
although it is possible to introduce them.
The basic idea is the following: the creation process (the @@create step) is
deferred as late as possible.
It will appear that, for built-in base classes like Array, creation occurs just
before initialisation, making the created-but-initialised state of such objects
unobservable, at least in absence of user-overridable @@create hook.
Non-Constructed Objects
-----------------------
Non-Constructed Objects are introduced in order to describe the state of
not-yet-defined this-bindings.
Non-Constructed Objects is probably not be the greatest approach to spec the
thing, especially when considering the awful Step 3 of InitializeThisBindings()
algorithm below;
it is just a convenient hack that allows me to expose the idea with minimal
change from the current specification draft.
A Non-Constructed Object is a special placeholder exotic object with the
following internal slots:
* [[Constructor]], which holds a reference to the constructor;
* [[NonConstructed]], set to `true`.
Non-Constructed Objects may only appear as value of this-bindings inside
functions
(or, using the spec language, as value of the `thisValue` component of a
function environment record).
Any attempt to get an explicit reference to such an object in user code will
throw an error.
The only way to pass (implicitely) a reference to a Non-Constructed Object
between different function environment records,
is through calls to `super`.
InitializeThisBindings(nonconstructedObj, obj) abstract operation
----------------------------------------------------------------------
This operation performs the actual initialisation of the this-bindings that
were previously deferred:
1. Assert `nonconstructedThisObj` is a Non-Constructed Object.
2. Assert `obj` is an ordinary object.
3. Replace all references to `nonconstructedThisObj` with references to
`obj`.
(In particular, this step will effectively initialise the this-binding of
every function environment record that used to reference `nonconstructedObj`.)
Additional runtime semantics of the `this` keyword
--------------------------------------------------
Any attempt to get an explicit reference to the `thisValue` of a function
environment record while it holds a Non-Constructed Object, shall throw an
error.
new C(...args)
----------------
When a constructor `C` is called with arguments `args`, the following steps are
taken.
In particular, the actual initialisation of the this-value is deferred.
1. Let `thisValue` be a new Non-Constructed Object, with its internal slot
[[Constructor]] set to `C`.
2. Let `R` be the result of `C`.[[Call]](`thisValue`, `args`).
3. NOTE. The operation InitializeThisBinding() may have been called during the
previous step, meaning that `thisValue` may now be a regular object.
4. ReturnIfAbrupt(`R`).
5. If Type(`R`) is Object, return `R`.
6. If `thisValue` is a Non-Constructed Object, throw an error.
7. Return `thisValue`.
No more [[Construct]]
----------------------
[[Construct]] internal method is gone. Actually it is conflated with the
[[Call]] internal method, modified as below.
The key fact is that [[Call]] is able to see if it should have a
[[Construct]]-like behaviour,
by examining whether its `thisArgument` is a Non-Constructed Object.
F.[[Call]] (thisArgument, argumentsList) for user-defined functions
-----------------------------------------------------------------------------------
User-defined functions has the currently specced [[Call]] behaviour [Section
9.2.2],
with the following additional step inserted somewhere near the beginning of the
algorithm, e.g., after step 1:
1bis. If the `thisArgument` is a Non-Constructed Object,
a. If `F`’s [[NeedsSuper]] internal slot is set to false (IOW, if `F`’s code
doesn’t contain `super`),
i. Let `proto` be the result of
GetPrototypeFromConstructor(`thisArgument`.[[Constructor]],
"%ObjectPrototype%").
ii. ReturnIfAbrupt(`proto`).
iii. Let `obj` be ObjectCreate(`proto`).
iv. Perform InitializeThisBindings(`thisArgument`, `obj`).
v. Assert: Now, `thisArgument` is an ordinary object.
b. Else, `thisArgument` is left untouched. // it is meant to be handled at
the occasion of the enclosed `super` call.
F.[[Call]] (thisArgument, argumentsList) for bound functions
-------------------------------------------------------------
In the algorithm sepcced in [Section 9.4.1.1], step 2 is replaced with:
2. If the `thisArgument` is a Non-Constructed Object, let `boundThis` be
`thisArgument`. // this is the current [[Construct]] behaviour
2bis. Else, let `boundThis` be the value of `F`’s [[BoundThis]] internal slot.
// this is the current [[Call]] behaviour
F.[[Call]] (thisArgument, argumentsList) for the built-in Object constructor
-----------------------------------------------------------------------------
There is no change: `Object(...)` acts as a factory rather than as a
constructor, as currently specified, and the `thisArgument` is ignored.
In particular trying to subclass `Object` will lead to unexpected results. Note
however that `new Object` does still work.
F.[[Call]] (thisArgument, argumentsList) for the built-in Array contsructor
---------------------------------------------------------------------------
The [[ArrayInitializationState]] internal slot is gone, and Step 4 of the
algorithms in [Sections 22.1.1.*] is replaced with (where `O` is the
this-value):
4. If `O` is a Non-Constructed Object,
a. Let `proto` be the result of GetPrototypeFromConstructor(`O`.[[Constructor]],
"%ArrayPrototype%").
b. ReturnIfAbrupt(`proto`)
a. Let `array` be ArrayCreate(<<length>>, `proto`).
b. Perform InitializeThisBindings(`thisArgument`, `array`)
5. Else, etc.
The [[Call]] behaviour of other built-in constructors is left as an exercise to
the reader.
Comments
--------
There is a nice side-effect of the proposal: The new internal check intended to
discriminate between call-as-function and call-as-constructor is easier and
more robust. In particular,
* hacks such as [[ArrayInitializationState]] are no longer needed;
* bound functions are truly subclassable (see [2]).
However, it remains very hard for user-defined functions to distinguish
correctly between constructor/initialisation-calls and method/function-calls,
or to write code that works well in both cases. (At least the situation is not
worse than in ES5-.)
Optional: the @@create hook
----------------------------
A @@create hook can easily be placed as follows:
In each [[Call]] internal methods defined above, a call to
(`thisArgument`.[[Constructor]]).@@create
could replace the steps spanning from GetPrototypeFromConstructor(...)
inclusive to InitializeThisBindings(...) exclusive.
Whether such a hook is compatible with, e.g., DOM constructors, is left to the
appreciation of the competent people. At worst, a built-in constructor could
cheat by defining its own [[Call]] internal method that would refuse to run the
@@create hook.
Optional: the @@new hook
-------------------------
Alternatively, the following hook may be installed:
At the beginning of each call to F.[[Call]], the following steps are taken:
1. if `thisArgument` is a Non-Constructed Object,
a. If `F` has an *own* property named @@new,
i. Let `R` be the result of
`F`[@@new].[[Call]](`thisArgument`.[[Constructor]], `argumentsList`).
ii. ReturnIfAbrupt(`R`).
iii. If Type(`R`) is Object,
α. InitializeThisBindings(`thisArgument`, `R`).
iv. Return `R`.
b. etc.
2. etc.
Note that we don’t look for inherited @@new property, in order to preserve the
initialise-at-latest-time behaviour.
—Claude
[Section 9.2.2]:
http://people.mozilla.org/~jorendorff/es6-draft.html#sec-ecmascript-function-objects-call-thisargument-argumentslist
[Section 9.4.1.1]: http://people.mozilla.org/~jorendorff/es6-draft.html#sec-call
[Sections 22.1.1.*]:
http://people.mozilla.org/~jorendorff/es6-draft.html#sec-array-constructor
[1]: http://esdiscuss.org/topic/new
[2]:
http://esdiscuss.org/topic/issue-when-subclassing-a-bound-function-used-as-constructor