At the July 2014 TC39 meeting, we presented a sketch of a new object instantiation design that was inspired from posting of Claude Pache. Here's the basic idea, as originally described by Claude:
If a constructor
C
usessuper
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. usingnew C(...args)
:
- During phase /* 1 */, the this-binding is uninitialised; trying to access it through an explicit
this
keyword 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.prototype
rather thansuper.prototype
, whereD
is the constructor on which thenew
operator was originally applied (the reference toD
being 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).
The main innovation present at the meeting was the introduction of a new syntactic token new*
that can be used to distigish a [[Construct]] initiated evaluation of a constructor from a [[Call]] initiated evaluation.
Subsequent to this meaning we extensively discussed, refined and polished the design. In addition to changing new*
to new^
there we made three other major changes:
- Using
new super()
rather thansuper()
as the invocation of the superclass constructor via [[Construct]] - Requiring an explicit
this=
rather than making the firstsuper()
call implicitly assign to this. - Allowing anything to be assigned to this from within the this TDZ of a constructor.
###Rationale for change #1 Consider a constructor such as:
class D extends B {
constructor () {
If (!new^) super();
if (Math.random() >0.5) super();
f(super());
}
}
Which of the super()
expressions will do [[Construct]] invocation and which will do [[Call]] invocation? How does it change if the code is refactored? What happens if a new is put in front of any of the above super()
expressions? Does it change the semantics of any, some, or all of them?
We already have enough confusions between "called as a function" and "called as a constructor". As part of this proposal, for the first time, ECMAScript will allow a constructor to dynamically determine which way it was called. In general x()
means invoke via [[Call]] and new x()
means invoke via [[Construct]]. It would terrible to further confuse things by introducing special situations where super()
means [[Construct]] and not [[Call]]. If the ES programmer is thinking I need to let my base constructor create an object, then new super()
, is exactly the appropiate idiom and the right way to think about it. Other languages that use super()
for this purpose don't have the language level distinction between [[Call]] and [[Construct]].
###Rationale for change #2
As discussed at the July meeting, super()
, would magically bind this if it occurs in the this TDZ. We should be adding additional "magic" behavors to ECMAScript: a hidden implicit binding construct. The intent of explicitly setting this is much clearer then super()
which might might or might not be intended to set this (or be a [[Call]] or [[Construct]] depending upon what has already happened in the method.
Semantically, alloowing super()
to mean either [[Construct]] or [[Call]] makes it impossible to statically make the determination of which is meant. Consider the following example:
constructor() {
let randomize = o => Math.random()>=0.5 ? !!o: !o;
if (randomizer(new^)) return super(); //is this a [[Call]] or [[Construct]]?
}
Changes #1 and #2 fit naturally together. Where in previous designs we expected programmer to know the magic and say"
constructor() {
super(); //looks like a [[Call]] acts as a [[Construct]] and sets this
this.newProp = ...
}
we now expect programmers to be explicit about their intent by saying:
constructor() {
this = new super(); //always a [[Construct]], explicitly binds ths
this.newProp = ...
}
No magic, everything is explicit, and it is possible to statically dermine whether a constructor is intended to perform its own allocation and binding of its this value.
###Rationale for change #3 Once you have #2, #3 naturally follows and as we've shown there are lots of use cases for constructors that don't just do the default ordinary allocation.
Change #3 also reduces the difference (and refactoring expense) between common constructor patterns and constructors that do special allocation steps:
Typically a constructor might look like this:
constructor(a,b) {
this = new super();
this.a = a;
this.b = b;
}
But without #3, a constructor that does special allocation might look like:
constructor(a,b) {
let newObj = new^.specialAllocationMethod();
newObj.a = a;
newObj.b = b;
return newObj.
}
With #3 such a constructor can be expressed as:
constructor(a,b) {
this = new^.specialAllocationMethod();
this.a = a;
this.b = b;
}
Only the this assignment is differnt from the typical construtor pattern.
Is that a typo?