This Gist presents a new design of class-based object construction in ES6 that does not require use of the two-phase @@create protocol.
One of the characteristics of this proposal is that subclass constructors must explicitly super invoke their superclass's constructor if they wish to use the base class' object allocation and initialization logic.
An alternative version of this design automatically invokes the base constructor in most situations.
In addition to the material below, there is a seperate design rationale covering several key features that are common to both designs.
The patterns below progressively explain the key semantic features of this proposal and their uses. A more complete summary of the semantics for this proposal are in Proposed ES6 Constructor Semantics Summary.
A class declaration without an extends clause and without a constructor method has a default constructor that allocates an ordinary object.
{};let b0 = ; //equivalent to: let b0 = Object.create(Base0.prototype);
The most common form of constructor automatically allocates an ordinary object, binds the object to this, and then evaluates the constructor body to initialize the object.
{thisa = a;}let b1 = 1; //equivalent to: let b1 = Object.create(Base1.prototype);// b1.a = 1;
Any constructor body that contains an explicit assignment to this does not perform automatic allocation. Code in the constructor body must allocate an object prior to initializing it.
{this = ; //create a new exotic array instanceObject;this0 = x;}{return true}let b2 = 42;;; //Array.isArray test for exotic array-ness.; //b2 has exotic array length property behavior;
Often derived classes want to just use the inherited behavior of their superclass constructor. If a derived class does not explicitly define a constructor method it automatically inherits its superclass constructor, passing all of its arguments.
{};let d0 = "b1";;;;
The above definition of Derived0 is equivalent to:
{this = ...arguments;}
Note that the this value of a constructor is the default value of the invoking new expression unless it is explicitly over-ridden by a return statement in the constructor body. (This is a backwards compatible reinterpretation of legacy [[Construct]] semantics, updated to take into account that the initial allocation may be performed within the constructor body.)
A derived class must explicitly set its this binding. Typically by first invoking its base class constructor using the new operator, to obtain a new instance. It then evaluates the remainder of the derived class constructor using the value returned from the base constructor as the this value.
{this = a; //note only a passed to super constructorthisb = b;};let d1 = 1 2;; //defined in Base1 constructor; //defined in Derived1 constructor
A derived class may perform arbitrary computations prior to calling its base constructor, as long as the computations don't reference this. For example, if the derived constructor needs to modify the arguments passed to the base constructor, it must perform the necessary computations prior to invoking the base constructor and assigning the result to thus.
{this = b a;thisc = c;};let d2 = 1 2;;;;
{let a = b = ; //note, can't reference 'this' here.this = a b;thisc = c;};let d3 = 3;;
{this = Object;thisa = a;}{return true};let d4 = 42;; //inherited from Base2;; //from Derived4; //not an exotic array object.;;
{return Proxy...arguments new^};static {return{console;return Reflect;};};;
Use of return is appropriate here because we don't need to reference this within the body of the constructor. Using return over-rides the uninitialized this binding.
Additional classes that use different handlers can be easily defined:
static {return Object;};;
Note that Derived6 doesn't need an explicit constructor method definition. Its automatically generated constructor performs this=new super(...arguments);
.
{this = undefined;}{console;static {console};};let ab;try ab = catche ;;
Classes derived from AbstractBase must explicitly allocate their instance objects.
{this = Object; //instances are ordinary objects}let d7 = ;d7; //logs messageD7; //logs message;;{};; //throws TypeError because result of new is undefined
A unique feature of ECMAScript is that a constructor may have distinct behaviors depending whether it is invoke by a new expression or by a regular function call expression.
When a constructor is called using a call expression, the token new^ has the value undefined within the constructor body.
{if new^ console;else console;}; //logs: Called "as a constructor" using the new operator.; //logs: Called "as a function" using a function expression.
{if !new^ return ;};;
{if !new^ throw Error"Not allowed to call this constructor as a function";}
{if new^//called as a constructorthisx = x;else//called as a functionreturn x;};let f2c = "xy";let f2f = ;&& f2cx ==="xy");;
The distinction between "called as a function" and "called as a constructor" also applies to super invocations.
{if new^this = x+x; //calls F2 as a constructorelsereturn superx + superx; //calls F2 as a function (twice)};let f3c = "xy";let f3f = ;&& f3cx ==="xyxy");;
A base class constructor that is known to perform automatic allocation may be called (as a function) by a derived constructor in order to apply the base initialization behavior to an instance allocated by the derived constructor.
{this = ;Object;superx; //note calling super "as a function", passes this,// and does not do auto allocation}let d8 = 8;;
However, care must be taken that the base constructor does not assign to this when "called as a function".
This construction framework breaks object construction into two phase, an allocation phase and an instance initialization phase. This framework design essentially duplicates the @@create design originally proposed for ES6. The design of this framework uses a static "@@create" method to perform the allocation phase. The @@create method may be over-ridden by subclass to change allocation behavior. This framework expects subclasses to place instance initialization logic into the constructor body and performs top-down initialization.
Symbolcreate = Symbol;{this = new^Symbolcreate;}static {// default object allocationreturn Object;}//A subclass that over rides instance initialization phase{this = ;// instance initialization logic goes into the constructorthisx = x;}//A subclass that over rides instance allocation phasestatic {// instance allocation logic goes into the @@create method bodylet obj = ;Object;return obj;}
This construction framework also breaks object construction into two phase, an allocation phase and an instance initialization phase. The design of this framework uses the constructor method to perform the allocation phase and expects subclasses to provide a separate initialization method to peform instance initialization. The initialize methods control whether initialization occur in a top-down or bottom-up manner.
{return this}{return this}//A subclass that over rides instance initialization phase{// instance initialization logic goes into an initialize methodthisx = x;}//A subclass that over rides instance allocation phase{// instance allocation logic goes into the constructor bodythis = ;Object;return this;}//A subclass that over rides instance initialization phase//and performs top-down super initialization{super; //first perform any superclass instance initializationthisx = x;}//A subclass that over rides instance initialization phase//and performs bottom-up super initialization{thisx = x; //first initialize the state of this instancesuper; //then perform superclass initialization}
{return Object;//better: this = Object.create(new^.prototype);//or: this = {__proto__: new^.prototype};}
JavaScript has always allowed a constructor to over-ride its automatically allocated instance by returning a different object value. That remains the case with this design. However, using return in this manner (instead of assigning to this) may be less efficient because the constructor is specified to still automatically allocates a new instance.
{thisy = y;}0; //reference error because this is in its TDZ
Derived classes don't perform automatic allocation, they must either perform this = new super(...arguments), locally allocate an object and assign it to **this, or explicitly return an object as the value of the constructor.
{this = /*new*/ superb a; //what if we forget to put in newthisc = c;};123; //ReferenceError
This is a derived constructor so it doesn't perform automatic allocation and this is initially uninitialized. It assigns to this but the super() call in its first statement implicitly references this before it is initialized so a ReferenceError exception will be thrown.
toMethod
.constructor
ConciseMethod within a class definition. It is also allowed within an ArrowFunction within the body of any such a function.There is a Rationale that further explains some of these design changes.
Any functions defined using a FunctionDeclaration, FunctionDeclaration, or a call to the built-in Function constructor can be invoked as a constructor using the new operator. We will call such functions "basic constructors".
{thisfoo = a;}let obj=42;;;
Because this basic function does not contains an explicit assignment to this, when it is invoked using new it performs default allocation before evaluating its function body. The default allocation action is to allocated an ordinary object using BasicF.prototype as the [[Prototype]] of the newly allocated object. After default allocation, the body of the function is evaluated with the newly allocated object as the value of this.
This is exactly the same behavior that such a function would have in prior editions of ECMAScript.
The [[Prototype]] of a basic constructor can be set to some other constructor function We will use the term "derived function" to describe basic constructor functions that have had their [[Prototype]] set in this manner and the term "base constructor" to describe a constructor that is the [[Prototype]] value of a derived function.
Turning a basic constructor into a derived constructor by setting its [[Prototype]] does not change the default allocation action of the basic constructor. It still allocates an ordinary object and then evaluates its body with the newly allocated object as the value of this. It does not automatically call the base constructor.
DerivedF__proto__ = BasicF;{thisbar = b;};let obj=42 43;;; //BaseF is not automatically called.;
The above behavior is also identical to that of previous versions of ECMAScript, assuming __proto__
is supported.
If a derived constructor wants to delegate object allocation and initialization to a base constructor it must over-ride default allocation using an expression that invokes the base constructor using the new operator and assign the result to this.
DerivedF2__proto__ = BasicF;{this = a;thisbar = b;};let objF2 = 42 43;
Design Rationale: Basic constructors differ from class constructors in that they always do automatic allocation and initialization of this, even if the constructor logically extends another constructor. This difference is necessary to preserve compatibility with existing JavaScript code that uses __proto__
to set the [[Prototype]] of constructor functions. Such code will have been written expecting new to enter the constructor with a fresh ordinary object rather than with this uninitialized in its TDZ.
{this = __proto__: Array length: 0; //instances are ordinary objects with a length property}var aLike = ;
Basic constructors may assign to this (this=
) to over-ride their default allocation action, just like constructors defined using a class definition. For example, all of the following class-based examples can be easily rewritten using basic constructors in place of the derived classes:
Permutated Arguments,
ComputedArguments,
Base Not Used,
Proxy Wrapping Base.
The new^ token may be used within a basic constructor to determine if it has been "called as a function" or "called as a constructor" and in the latter case to access the receiver value pass to it via [[Construct]].
See Semantics Summary for an explanation of the new^ token.
{"use strict";console;}{"use strict";super;console;}derived__proto__=base;;//logs: in base, this=undefined//logs: in derived, this=undefined
The current this value (undefined in this example) is implicitly passed as the this value of a super()
call.
{if !new^ return ...args;thismessage = "Created by SelfNewing";};console; //logs: Created by SelfNewingconsole; //logs: Created by SelfNewing
####Anti-pattern: Qualified super references in basic constructors A basic constructor can not use qualified super references unless it has been installed as a method using toMethod or Object.assign.
{return super}try // ReferenceError [[HomeObject]] binding not defined.catch e console; //logs: ReferenceErrorvar obj=__proto__: {console};objf=f;obj; //logs: foo
Up to now, the ES6 specification has allowed super without a property qualification to be used within methods as a short hand for a super-based property access using the same property name as the name of the current method. For example:
{super; //means the same thing as: super.foo();//do more stuff}
This is a convenient short hand because calling the super method of the same name is, by far, the most common use case for super invocation in a method. It is actually quite rare for a method to want to super call any name other than its own. Some languages, that have a super, only allow such unqualified super invocations.
In looking at what it takes to rationally integrate FunctionDeclaration/FunctionExpression defined constructors into the new ES6 object instantiation design I've concluded that in order to give new super
and super()
rational meaning inside such basic constructors we need to eliminate the current implicit method name qualification of super in other contexts. I suspect some people will be happy about this, and some will be sad.
There are a couple of things that drove me to this (reluctant) decision.
First (and perhaps least important) is the fact that within a class constructor when somebody says new super
they really mean precisely the constructor that was identified in the extends clause of the actual constructor function. This is also the value used to set the class constructor's [[Prototype]]. new super.consructor
usually means the same thing, but if somebody does rewiring of the prototype chain or modifies the value of the 'constructor' property this may not be the case.
If a constructor is defined using a function definition like:
C__proto__ = Array;{this = ;}Cprototype = __proto__: Arrayprototype{...}//note missing constructor property definition;
we need a semantics for new super
that doesn't depend upon 'toMethod' first being invoked on C or on C.prototype.constructor
being defined. It pretty clear that in a case like this the developer intends that new super()
would invoke Array.[[Construct]].
The interpretation of new super
that meets this criteria and which can be consistently applied to both constructors defined using a class definition and constructors defined using function definitions is that new super
means the same thing as new (<currentFunction>.[[GetPrototypeOf]]()
. In other words, new super
should follow the current constructor function's [[Prototype]] chain. If <currentFunction>.[[Prototype]]
is null it should throw. If <currentFunction>.[[Prototype]]
is Function.prototype it will allocated an ordinary object.
What if somebody writes
C__proto__ = Array;{if new^ return ;return super;}
I don't think we want the two unqualified super references to bind to different values. Or for super()
to mean different things depending upon whether the enclosing function was invoked via [[Construct]] or [[Call]]. And deleting the first line of C really shouldn't change the semantics of the second line.
Also, I don't think we want the meaning of super()
to change if somebody does:
someObjfoo= C;
Finally,
{super}
shouldn't mean something different from:
{};Aprototype {super};
As far as I could find, the best way out of this is to completely eliminate implicit method name qualification of super in non-constructor concise methods and only allow unqualified super in constructors. Basically, you will have to write
{super; //can't say: super()}
if that is what you mean. This make ES6, in this regard, more like Java (and Smalltalk) and less like Ruby.