ES6 classes: deferring the creation step
Please do the following substitutions in my message:
- "created-but-initialised" → "created-but-not-initialised"
- "an ordinary object" → "an object (i.e. a value of type Object) that is not a Non-Constructed Object" (2x)
—Claude
Le 27 juin 2014 à 15:56, Claude Pache <claude.pache at gmail.com> a écrit :
I like it! Cc'ing others who may have missed it. Boris is DOM guru you seek.
Does it address the bound function issue you cited in the previous thread? It appears not to, but I might be missing something (jetlag).
Brendan Eich wrote:
I like it! Cc'ing others who may have missed it. Boris is DOM guru you seek.
Forgot to suggest a better spec-name: "Pre-constructed" instead of "Non-Constructed".
Le 28 juin 2014 à 17:49, Brendan Eich <brendan at mozilla.org> a écrit :
I like it! Cc'ing others who may have missed it. Boris is DOM guru you seek.
Does it address the bound function issue you cited in the previous thread? It appears not to, but I might be missing something (jetlag).
It does address the issue (if my reasoning is correct).
Rethinking on the subject, I think that the intended semantics would have appeared more clearly, if I had kept a modified [[Construct]] internal method separate from the [[Call]] one. Then, the new operator would invoke [[Construct]], but, more notably, super would also invoke [[Construct]] when the this-binding is not yet initialised (i.e., when this is a Non-Constructed Object). — super invocations occurring when this is initialised regularly invoke [[Call]].
So, roughly, the first super in a constructor would invoke the super-constructor with the semantics of a constructor rather than of a method. Therefore, subclassing objects with even wildly different [[Construct]] and [[Call]] behaviours would just work.
Also, the @@new hook would just have been an exact override of the modified [[Construct]] internal method. (Which is another reason why I should have kept [[Construct]] and [[Call]] separate.)
This looks nice to me as well. But given the length I had trouble internalizing in what externally-visible ways it would change the status quo, especially with the optional @@create addition. The writeup as-is is very spec-language-focused. Would it be simple to summarize that for us?
There's also the question of what implementers think. To me it seemed a little more "weird" than @@create, such that the exact implementation details might not straightforwardly translate to implementations. But then again, implementers generally dislike @@create, so I don't think my intuition here is very good.
Thanks Claude for working this up.
InitializeThisBindings(nonconstructedObj, obj) abstract operation
This operation performs the actual initialisation of the this-bindings that were previously deferred:
- Assert
nonconstructedThisObjis a Non-Constructed Object.- Assert
objis an ordinary object.- Replace all references to
nonconstructedThisObjwith references toobj. (In particular, this step will effectively initialise the this-binding of every function environment record that used to referencenonconstructedObj.)
I was vaguely thinking along similar lines last week, but was stumped at this point. Is step #3 possible? You'd basically have to (magically?) replace the "this" binding for any subclass constructors on the call stack. This is probably a question for Allen.
Le 29 juin 2014 à 00:36, Domenic Denicola <domenic at domenicdenicola.com> a écrit :
This looks nice to me as well. But given the length I had trouble internalizing in what externally-visible ways it would change the status quo, especially with the optional @@create addition. The writeup as-is is very spec-language-focused. Would it be simple to summarize that for us?
I plan to rewrite new version of the proposal very soon, with a less spec-language-oriented description (and with some amendments for better handling edge cases).
The problem of observability of created-but-not-initialised instances is not exactly solved in presence of the @@create hook. I think that the main interest is that without that hook, subclassing does work and the aforementioned problem is solved.
Le 29 juin 2014 à 04:46, Kevin Smith <zenparsing at gmail.com> a écrit :
Thanks Claude for working this up.
InitializeThisBindings(nonconstructedObj, obj) abstract operation
This operation performs the actual initialisation of the this-bindings that were previously deferred:
- Assert
nonconstructedThisObjis a Non-Constructed Object.- Assert
objis an ordinary object.- Replace all references to
nonconstructedThisObjwith references toobj. (In particular, this step will effectively initialise the this-binding of every function environment record that used to referencenonconstructedObj.)I was vaguely thinking along similar lines last week, but was stumped at this point. Is step #3 possible? You'd basically have to (magically?) replace the "this" binding for any subclass constructors on the call stack. This is probably a question for Allen.
In fact, what I really need, is that the this-binding acts like a const-binding: It may be uninitialised at first, and will be initialised after the first call to a super-method (in most non-buggy code, it will be the super-constructor).
Le 27 juin 2014 à 15:56, Claude Pache<claude.pache at gmail.com> a écrit :
Here is a counter-proposal (or an improved proposal, ad libidum), which is not tightly coupled to @@new.
This proposal seems ok to me from my DOM-centric perspective. ;)
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.
The problem with @@create is not so much whether built-in constructors call it. It's whether user code can invoke built-in @@create hooks and hence observe partially-constructed object.
However, with this proposal it seems easy enough to just not provide any such built-in @@create hooks, right?
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 */
super(...whatever)
/* 3: the rest of the code */
// ...
}
}
the following occurs when C is invoked as constructor,
e.g. using new C(...args):
-
During phase /* 1 */, the this-binding is uninitialised; trying to access it through an explicit
thiskeyword will throw a ReferenceError. -
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.prototyperather thansuper.prototype, whereDis the constructor on which thenewoperator was originally applied (the reference toDbeing forwarded by the super-call as needed). -
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
thisArgumentif 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
thisConstructorvalue 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.prototyperather thanF.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]]:
ReturnNormalizedConstructCompletion(`R`).
as an abbreviation for:
- If
Ris an abrupt completion, returnR. - Else, if Type(
R.[[value]]) is Object, returnCompletion{[[type]]: normal, [[value]]: R.[[value]], [[thisValue]]: R.[[value]]}. - Else, if
R.[[thisValue]]is present and Type(R.[[thisValue]]) is Object, returnCompletion{[[type]]: normal, [[value]]: R.[[thisValue]], [[thisValue]]: R.[[thisValue]]}. - Else, throw a ReferenceError, with its
[[thisValue]]field set toR.[[thisValue]]ifR.[[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
Fdoes not usesuper(that is, in spec language, ifF.[[NeedsSuper]]is false):- Let
objbe the result of OrdinaryCreateFromConstructor(receiver.prototype, "%ObjectPrototype").
// this is roughlyobj = Object.create(receiver.prototype || Object.prototype) - ReturnIfAbrupt(
obj). - Let
resultbe the result ofF.[[Call]](obj, argumentsList, receiver). - ReturnNormalizedConstructCompletion(
result).
- Let
-
If
Fusessuper, the creation step is deferred:- Let
resultbe the result ofF.[[Call]](empty, argumentsList, receiver). - ReturnNormalizedConstructCompletion(
result).
- Let
Semantics of a super-method call
When, say, super.method(..args) is called, the following steps are taken:
- Let
Fbe the method referenced bysuper.method. - Let
thisValue = GetThisBinding(). - If
thisValueis not empty,
a. Letresultbe the result ofF.[[Call]](thisValue, args). - Else,
a. Letreceiver = GetThisConstructor().
b. Letresultbe the result ofF.[[Construct]](receiver, args).
c. If Type(result.[[thisValue]]) is Object,
i. InitializeThisBinding(result.[[value]]).
d. Assert: If the test in previous step was negative, thenresultis an abrupt completion. - Return
result.
Special behaviour of the Object constructor
The [[Construct]] internal method of Object:
Object.[[Construct]] (receiver, argumentsList)
has the following semantics:
- If
receiverisObject,
a. LetresultbeObject.[[Call](undefined, argumentsList).
// this is the usual factory function. - Else,
a. Letresultbe OrdinaryCreateFromConstructor(receiver, "%ObjectPrototype").
// which is roughlyObject.create(receiver.prototype || Object.prototype) - 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.
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
thisArgumentif 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.
Hmm, mixed feelings about extending the Completion record type to hold another field. It could be problematic for implementors to store this additional state efficiently. On second thought, it should be possible to merge [[thisValue]] with the existing [[Value]] field, and conditionally handle [[Value]] == empty in [[Construct]]? [1]
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
thisArgumentif 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.
Tail-call semantics may interfere with determining the value of "this-binding at the time of completion", because the execution context was already popped from the stack at that time. Probably solvable by determining the this-binding before performing the tail-call, plus some special casing when the tail-call expression is a super-call.
When, say,
super.method(..args)is called, the following steps are taken:
- Let
Fbe the method referenced bysuper.method.
This part needs to be redefined. super.method is an abbreviation for:
- Let env be GetThisEnvironment().
- Let baseValue be GetSuperBase(env).
- Let actualThis be GetThisBinding().
- Let result be baseValue.[[Get]]("method", actualThis).
But at this-binding initialisation time, actualThis is still the
empty placeholder object, so it cannot be used as the receiver argument
for the [[Get]] internal method call.
- During phase /* 1 */, the this-binding is uninitialised; trying to access it through an explicit
thiskeyword will throw a ReferenceError.
This seems overly restrictive to me. The common case (as in ES5) will be classes that derive from Object, where no such restriction is necessary.
The crux of the problem is that some host-defined classes that cannot safely separate object allocation from initialization. What if the @@create hook was allowed to return undefined? In such a case, your "uninitialized" semantics would apply: attempting to dereference "this" before calling super(...) would throw an error, and super(...) would essentially set the "this" value? In all other cases, the currently drafted semantics would apply.
(Sorry for the delay of response, I am currently in a remote area.)
Le 7 juil. 2014 à 19:02, Kevin Smith <zenparsing at gmail.com> a écrit :
- During phase /* 1 */, the this-binding is uninitialised; trying to access it through an explicit
thiskeyword will throw a ReferenceError.This seems overly restrictive to me. The common case (as in ES5) will be classes that derive from Object, where no such restriction is necessary.
(To be more precise, the intended restriction is a TDZ, just like const declarations.)
There are trade-offs that should be made. Are there many cases where you want to manipulate this before calling super?
The crux of the problem is that some host-defined classes that cannot safely separate object allocation from initialization. What if the @@create hook was allowed to return undefined? In such a case, your "uninitialized" semantics would apply: attempting to dereference "this" before calling super(...) would throw an error, and super(...) would essentially set the "this" value? In all other cases, the currently drafted semantics would apply.
One feature of the proposal is that the first call to super invokes the constructor with the semantics of [[Construct]] rather than [[Call]], so that subclassing classes that have different construct/call behaviour would just work. It implies that there is no preliminary creation step.
(Sorry for the delay of response, I am currently in a remote area.)
Le 6 juil. 2014 à 20:15, André Bargull <andre.bargull at udo.edu> a écrit :
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
thisArgumentif 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.
Hmm, mixed feelings about extending the Completion record type to hold another field. It could be problematic for implementors to store this additional state efficiently. On second thought, it should be possible to merge [[thisValue]] with the existing [[Value]] field, and conditionally handle [[Value]] == empty in [[Construct]]? [1]
An issue here is that I want to initialise the this-binding of the caller even when the callee ends abruptly (with throw).
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
thisArgumentif 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.
Tail-call semantics may interfere with determining the value of "this-binding at the time of completion", because the execution context was already popped from the stack at that time. Probably solvable by determining the this-binding before performing the tail-call, plus some special casing when the tail-call expression is a super-call.
When, say,
super.method(..args)is called, the following steps are taken:
- Let
Fbe the method referenced bysuper.method.This part needs to be redefined.
super.methodis an abbreviation for:
- Let env be GetThisEnvironment().
- Let baseValue be GetSuperBase(env).
- Let actualThis be GetThisBinding().
- Let result be baseValue.[[Get]]("method", actualThis).
But at
this-binding initialisation time,actualThisis still the empty placeholder object, so it cannot be used as the receiver argument for the [[Get]] internal method call.
Here is another setting that I've thought of: When super is called with a not-yet-initialised this-binding, the this-binding of the callee is in fact a special pointer to the function environment record of the caller, and when the callee wants to read or update it's this-binding, it reads or update the one of the caller.
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:
true.Non-Constructed Objects may only appear as value of this-bindings inside functions (or, using the spec language, as value of the
thisValuecomponent 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 tosuper.InitializeThisBindings(nonconstructedObj, obj) abstract operation
This operation performs the actual initialisation of the this-bindings that were previously deferred:
nonconstructedThisObjis a Non-Constructed Object.objis an ordinary object.nonconstructedThisObjwith references toobj. (In particular, this step will effectively initialise the this-binding of every function environment record that used to referencenonconstructedObj.)Additional runtime semantics of the
thiskeywordAny attempt to get an explicit reference to the
thisValueof a function environment record while it holds a Non-Constructed Object, shall throw an error.new C(...args)
When a constructor
Cis called with argumentsargs, the following steps are taken. In particular, the actual initialisation of the this-value is deferred.thisValuebe a new Non-Constructed Object, with its internal slot [[Constructor]] set toC.Rbe the result ofC.[[Call]](thisValue,args).thisValuemay now be a regular object.R).R) is Object, returnR.thisValueis a Non-Constructed Object, throw an error.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
thisArgumentis 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
thisArgumentis a Non-Constructed Object, a. IfF’s [[NeedsSuper]] internal slot is set to false (IOW, ifF’s code doesn’t containsuper), i. Letprotobe the result of GetPrototypeFromConstructor(thisArgument.[[Constructor]], "%ObjectPrototype%"). ii. ReturnIfAbrupt(proto). iii. Letobjbe ObjectCreate(proto). iv. Perform InitializeThisBindings(thisArgument,obj). v. Assert: Now,thisArgumentis an ordinary object. b. Else,thisArgumentis left untouched. // it is meant to be handled at the occasion of the enclosedsupercall.F.[[Call]] (thisArgument, argumentsList) for bound functions
In the algorithm sepcced in Section 9.4.1.1, step 2 is replaced with:
thisArgumentis a Non-Constructed Object, letboundThisbethisArgument. // this is the current [[Construct]] behaviour 2bis. Else, letboundThisbe the value ofF’s [[BoundThis]] internal slot. // this is the current [[Call]] behaviourF.[[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 thethisArgumentis ignored. In particular trying to subclassObjectwill lead to unexpected results. Note however thatnew Objectdoes 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
Ois the this-value):Ois a Non-Constructed Object, a. Letprotobe the result of GetPrototypeFromConstructor(O.[[Constructor]], "%ArrayPrototype%"). b. ReturnIfAbrupt(proto) a. Letarraybe ArrayCreate(<<length>>,proto). b. Perform InitializeThisBindings(thisArgument,array)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,
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:
thisArgumentis a Non-Constructed Object, a. IfFhas an own property named @@new, i. LetRbe the result ofF[@@new].[[Call]](thisArgument.[[Constructor]],argumentsList). ii. ReturnIfAbrupt(R). iii. If Type(R) is Object, α. InitializeThisBindings(thisArgument,R). iv. ReturnR. b. etc.Note that we don’t look for inherited @@new property, in order to preserve the initialise-at-latest-time behaviour.
—Claude
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