You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Auto super: Alternative Design where subclass constructors automatically call their superclass constructors
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 subclasses constructors automatically super invoke their superclass constructor's object allocation and initialization logic unless the derived constructor expliictly assigns to this.
An alternative version of this proposal eliminates from the design most implicit/automatic invocations of superclass constructors.
In addition to the material below, there is a seperate design rationale covering several key features that are common to both designs.
Constructors may be invoked either as a "function call" or by a new operator
function call: Foo(arg)
new operator: new Foo(arg)
Within a constructor, the new^ token can be used as a PrimaryExpression to determine how the construtor was invoked
If the value of new^ is undefined then the current function invocation was as a function call.
If the value of new^ is not undefined then the current function invocation was via the new opertor and the value of new^ is the constructor functon that new was originally applied to .
When a constructor is invoked using the new operator it has two responsibility
Allocate (or otherwise provide) a this object
Initialize the this object prior to returning it to the caller
The allocation step may be performed automatially prior to evaluating the constructor body or it may be manually performed by code within the consturctor body.
Automatic allocation is the default.
1. The value of this is initialized to the result of automatic allocation prior to evaluating the body of the constructor.
If a constructor body contains an assignment of the form this= then automatic allocation is not performed and the constructor is expected to perform manual allocation.
1. The value of this is uninitialized (in its TDZ) upon entry to the body of a manually allocating constructor.
2. Referencing (either explicitly or implicitly) an unitialized this will throw a ReferenceError.
2. The first evaluation of an assignment to this initializes it.
3. this may be dyanmically asigned to only once within an evaluation of a manaully allocating constructor body.
4. When a constructor is invoked via a function call (ie, via [[Call]]), this is preinitialized using the normal function call rules. Any dynamic assignment to this during such an invocation will throw a ReferenceError.
Automatic allocation actions:
For a constructor defined using a FunctionDeclaration, FunctionExpression, or by a class definition that does not have an extends clause, the automatic allocation action is to allocated an ordinary object.
For a constructor defined by a class definition that includes a extends clause, the automatic allocation action is to perform the equivalent of: new super(...arguments)
Executing newsuper in a constructor (that was itself invoked using the new operator) invokes its superclass constructor using the same new^ value as the invoking constructor.
Usage Pattern for a Proposed New ES6 Constructor Semantics
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.
Allocation Patterns
A base class with default allocation that needs no constructor logic
A class declaration without an extends clause and without a constructor method has a default constructor that allocates an ordinary object.
classBase0{};letb0=newBase0;//equivalent to: let b0 = Object.create(Base0.prototype);
A base class with default allocation and a simple constructor
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.
classBase1{constructor(a){this.a=a;}}letb1=newBase1(1);//equivalent to: let b1 = Object.create(Base1.prototype);// b1.a = 1;
A base class that manually allocates an exotic object
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.
classBase2{constructor(x){this=[];//create a new exotic array instanceObject.setPrototypeOf(this,new^.prototype);this[0]=x;}isBase2(){returntrue}}letb2=newBase2(42);assert(b2.isBase2()===true);assert(Array.isArray(b2)===true);//Array.isArray test for exotic array-ness.assert(b2.length==1);//b2 has exotic array length property behaviorassert(b2[0]==42);
A derived class with that adds no constructor logic
Often derived classes want to just use the inherited behavor of their superclass constructor. If a derieved class does not explicitly define a constructor method it automatially inherits its superclass constructor, passing all of its arguments..
Note that the this value of a constructor is the default value of the invoking new expression unless it is explicilty over-ridden by a return statement in the constructor body. (This is a backwards compatabible reinterpretation of legacy [[Construct]] semantics, updated to take into account that the initial allocation may be performed within the constructor body.)
A derived class that extends the behavior of its base class constructor
The default behavor of a constructor in a derived class is to first invoke its base class constructor using the new operator to obtain a new instance. It then evaluates the derived class constructor using the value returned from the base constructor as the this value.
classDerived1extendsBase1{constructor(a,b){this.b=b;}};letd1=newDerived1(1,2);assert(d1.a===1);//defined in Base1 constructorassert(d1.b===2);//defined in Derived1 constructor
The above definition of Derived1 is equivalent to:
A derived class that invokes its base constructor with permutated arguments
When a derived class automatically invokes its base constructor it passes all of the arguments from the new expression to the base constructor. If the derived constructor needs to modify the arguments passed to the base constructor, it must perform the necessary conputations prior to explicitly invoking the base constructor using new super and then assign the result to thus
A derived class that doesn't use its base class constuctor
classDerived4extendsBase2{constructor(a){this=Object.create(new^.prototype);this.a=a;}isDerived4(){returntrue};}letd4=newDerived4(42);assert(d4.isBase2()===true);//inherited from Base2assert(d4.isDerived4()===true);;//from Derived4assert(Array.isArray(d4)===false);//not an exotic array object.assert(d4.hasOwnProperty("length")===false);assert(d4.a==42);
A derived class that wraps a Proxy around the object produced by its base constructor
Use of return is appropiate here because we want the result of automatic allocation for use as the Proxy target. Using return allows us to perform automatic allocation and still return a different value from the constructor.
Additional classes that use different handlers can be easily defined:
classDerived6extendsDerived5{statichandler(){returnObject.assign({set(target,key,value,receiver){console.log(`set property: ${key} to: ${value}`);returnReflect.get(target,key,value,receiver)}},super.handler());};};
Note that Derived6 doesn't need an explicit constructor method definition. Its automatically generated constructor performs this=new super(...arguments); as its automatic allocation action.
####An Abstract Base class that can't be instantiated using new
classAbstractBase{constructor(){this=undefined;}method1(){console.log("instance method inherited from AbstractBase");staticsmethod(){console.log("class method inherited from AbstractBase")};};letab;try{ab=newAbstractBase}{catch(e){assert(einstanceofTypeError)};assert(ab===undefined);
Classes derived from AbstractBase must explicit allocate their instance objects.
classD7extendsAbstractBase{constructor(){this=Object.create(new^.prototype);//instances are ordinary objects}}letd7=newD7();d7.method1();//logs messageD7.smethod();//logs messageassert(d7instanceofD7);assert(d7instanceofAbstractBase);classD8extendsAbstractBase{};newD8;//throws TypeError because result of new is undefined
Constructor Called as Function Patterns
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.
Detecting when a constructor is called as a function
When a constructor is called using a call expression, the token new^ has the value undefined within the constructor body.
classF0{constructor(){if(new^)console.log('Called "as a constructor" using the new operator.');elseconsole.log('Called "as a function" using a function expression.');}}newF0;//logs: Called "as a constructor" using the new operator.F0();//logs: Called "as a function" using a function expression.
A constructor that creates new instances when called as a function
A constructor that refuses to be called as a function
classNonCallableConstructor{constructor(){if(!new^)throwError("Not allowed to call this constructor as a function");}}
A constructor with distinct behavior when called as a function
classF2{constructor(x){if(new^){//called as a constructor
this.x=x;}else{//called as a functionreturnx.reverse();}}};letf2c=newF2("xy");letf2f=F2("xy");assert(typeoff2c=="object")&&f2c.x==="xy");assert(f2f==="yx");
super calls to constructors as functions
The distinction between "called as a function" and "called as a constructor" also applies to super invocations.
classF3extendsF2{constructor(x){if(new^){
this =newsuper(x+x);//calls F2 as a constructor}else{returnsuper(x)+super(x);//calls F2 as a function (twice)}};letf3c=newF3("xy");letf3f=F3("xy");assert(typeoff3c=="object")&&f3c.x==="xyxy");assert(f3f==="yxyx");
Calling a superclass constructor to perform instance initialization.
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.
classD8extendsBase1{constructor(x){this=[];Object.setPrototypeOf(this,new^.prototype);super(x);//note calling super "as a function", passes this,// and does not do auto allocation}}letd8=newD8(8);assert(d8.x==8);
However, care must be taken that the base constructor does not assign to this when "called as a function".
Patterns for Alternative Construction Frameworks
Two phase construction using @@create method
This construction framework breaks object construction into two phase, an allocation phase and an instance initializaiton 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 consturctor body and performs top-down initializaiton.
Symbol.create=Symbol.for("@@create");classBaseForCreate{constructor(){this=new^[Symbol.create]();}static[Symbol.create](){// default object allocationreturnObject.create(this.prototpe);}}classDerivedForCreate1extendsBaseForCreate{//A subclass that over rides instance initialization phaseconstructor(x){// instance initialization logic goes into the constructorthis.x=x;}}classDerivedForCreate2extendsBaseForCreate{//A subclass that over rides instance allocation phasestatic[Symbol.create](){// instance allocation logic goes into the @@create method bodyletobj=[];Object.setPrototypeOf(obj,this.prototype);returnobj;}}
Two phase construction using initialize method
This construction framework also breaks object construction into two phase, an allocation phase and an instance initializaiton phase. The design of this framework uses the constructor method to perform the allocation phase and expects subclasses to provide a seperate initializaiton method to peform instance initialization. The initialize methods control whether initialization occur in a top-down or buttom-up manner.
classBaseForInit{constructor(...args){returnthis.initialize(...args)}initialize(){returnthis}}classDerivedForInit1extendsBaseForInit{//A subclass that over rides instance initialization phaseinitialize(x){// instance initialization logic goes into an initialize methodthis.x=x;}}classDerivedForInit2extendsBaseForInit{//A subclass that over rides instance allocation phaseconstructor(...args){// instance allocation logic goes into the constructor bodythis=[];Object.setPrototypeOf(this,new^.prototype);returnthis.initialize(...args);}}classDerivedForInit3TextendsDerivedForInit1{//A subclass that over rides instance initialization phase//and performs top-down super initializationinitialize(x){super.initialize();//first perform any superclass instance initizationthis.x=x;}}classDerivedForInit3BextendsDerivedForInit1{//A subclass that over rides instance initialization phase//and performs bottom-up super initializationinitialize(x){this.x=x;//first initialize the state of this instancesuper.initialize();//then perform superclass initization}}
Some AntiPatterms
Using return instead of this to replace default allocation
classAnti1{constructor(){returnObject.create(new^.prototype);//better: this = Object.create(new^.prototype);//or: this = {__proto__: new^.prototype};}}
JavaScript has always allowed a constructor to over-ride its autmatically 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.
This is more troublesome if the constructor is in a derived class, because the automatic initialization action uses the new operator to invoke the superclass constructor and this action may have observable side-effects:
classNoisyBase{constructor(){console.log("NoisyBase")}}classAnti2extendsNoisyBase{constructor(){returnObject.create(new^.prototype);//better: this = Object.create(new^.prototype);}}newAnti2();//logs: NoisyBase
The default behavior of a ES6 constructor is to invoke the superclass constructor prior to evaluating the constructor body of the subclass. If a programmer is in the habit of coding in a language that requires explicit super calls in constructors, they may incorrectly include such a call in an ES6 consturctor. This is incorrect for two reasons, first it's not necessary and may have unintended side-effects; and second they are calling the superclass constructor "as a function" rather than "as a constructor".
If the superclass constructor simply initializes some properties this may be relatively benign, doing nothing worse than initializing the properties twice to the same value. However, if the superclass constructor has distinct "called as a function" behavior, that behavior will be invoked and possibly produce undesired side-effects.
A base class can be used to help find such undesirable super calls.
classAnti4extendsNonCallableConstructor{constructor(){super();}}newAnti4();//Error: Not allowed to call this constructor as a functionAnti4();//Error: Not allowed to call this constructor as a function
Calling super() instead of invoking super() with new
classDerived2BadextendsDerived1{constructor(a,b,c){this=/*new*/super(b,a);//what if we forget to put in newthis.c=c;}};newDerived2Bad(1,2,3);//ReferenceError
This constructor assigns to this so it won't perform automatic allocation and enters the constructor body with an uninitialized this value. However, the super() call in its first statement implicitly references this before it is initialized so a ReferenceError exception will be thrown.
Summary of revised semantics for super based references
The semantics of super used as the base for a property access (eg, super.name or super[expr] have not changed.
Requires that a [[HomeObject]] has been set for the function, either via a concise method definition or dynamcially using toMethod.
Starts property lookup at [[Prototype]] of [[HomeObject]] value.
Passes current this value.
super without a property access qualifier is only allowed within a constructor: function defined using a FunctionDeclaration, FunctionExpression, a call to the Function constructor, or the constructorConciseMethod within a class defintion. It is also allowed within an ArrowFunction within the body of any such a function.
In any other function, the occurrance of super without a property access qualifier is an early syntax error.
This is a change. Previously an unqualifed super could occur any where and was implicitly a propety access using the [[MethodName]] value as the property key.
Within a constructor an unqualifed super may only be used in one of these forms:
NewExpression: new super
NewExpression: new superArguments
CallExpression: superArguments
Within a constructor an unqualified super reference has the value of: currentFunction.[[GetPrototypeOf]](). This value is know as the "superclass constructor".
Referencing the superclass constructor, when its value is null, throws a TypeError expression.
A new super or new superArguments expression invokes the superclass constructor's [[Construct]] internal method passing as the receiver argument, the current value of new^.
A superArguments expression invokes the superclass constructor's [[Call]] internal method passing the caller's this value as the this parameter.
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".
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 functon 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.
A basic constructor that inherits from another constructor
The [[Prototype]] of a basic constructor can be set to some other construtor function, We will use the term "derived function" to describe basic construtor 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 automatially call the base constructor.
DerivedF.__proto__=BasicF;functionDerivedF(a,b){this.bar=b;}assert(DerivedF.__proto__===BasicF);letobj=newDerivedF(42,43);assert(obj.__proto__)===DerivedF.prototype);assert(obj.hasOwnProperty("foo")===false);//BaseF is not automatiically called.assert(obj.bar===43);
The above behavior is also identical to that of previous verions 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;functionDerivedF2(a,b){this=newsuper(a);this.bar=b;}assert(DerivedF2.__proto__===BasicF);letobjF2=newDerivedF2(42,43);assert(objF2.__proto__)===DerivedF2.prototype);assert(objF2.hasOwnProperty("foo"));//BaseF was explicitly called and initialized foo.assert(objF2.foo===42);assert(objF2.bar===43);
Design Rationale: Basic constructors differ from class constructors in that they do not automatically invoke new on their base constructor as their default allocation action. Instead they allocate an ordinary object. This difference is necessary to preserve compatability 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 the result of an automatic new invocation of the constructor's [[Prototype]].
Basic construtors may explicitly over-ride their default allocation actions.
functionArrayLike(){this={__proto__: Array,length: 0};//instances are ordinary objects with length property}varalike=newArrayLike().push(1,2,3,4,5);
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 rewritten using basic constructors in place of the derived classes:
Permutated Arguments,
ComputedArguments,
Base Not Used,
Proxy Wrapping Base.
####Basic construtor access to new^
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]].
Calling base constructor "as a function" using super
functionbase(){"use strict";console.log(`in base, this=${this}`);}functionderived(){"use strict";super();console.log(`in derived, this=${this}`);}derived.__proto__=base;derived();//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.
Self-newing Basic Constructors
functionSelfNewing(...args){if(!new^)returnnewSelfNewing(...args);this.message="Created by SelfNewing";};console.log(newSelfNewing().message);//logs: Created by SelfNewingconsole.log(SelfNewing().message);//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.
functionf(){returnsuper.foo()}try{f()}// ReferenceError [[HomeObject]] binding not defined.catch(e){console.log(e)};//logs: ReferenceErrorvarobj={__proto__: {foo(){console.log("foo")}}};obj.f=f.toMethod(obj);obj.f();//logs: foo
##Rationale for not allowing unqualifed super in methods.
####Previous ES6 design
Up to now, the ES6 specification has allowed super without a property qualificaton 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:
classDerivedextendsBase{foo(){super();//means the same thing as: super.foo();//do more stuff}}
This is a convient 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.
####Apply the new instantiation design to non-class defined constructors.
In looking at what it takes to rationally integrate FunctionDeclaraation/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:
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() will 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.
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:
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 unqualifed super in constructors. Basically, you will have to write
Typo: "The distinction between "called as a function" and "called as a function" also applies to super invocations."