Sequence of super()?

class A{
	name = "class A ";
	constructor(){
		this.name = this.name + "A!";
	}
}

console.log((new A()).name);

Show class A A!
fine. It sets the name, then call the constructor(). But

class A{
   name = "class A ";
   constructor(){
   	this.name = this.name + "A!";
   }
}

class B extends A{
   name = "class B ";
   constructor(){
   	super();
   	this.name = this.name + "B!";
   }
}

console.log((new B()).name);

It shows class B B! ....why? and I expect class B A!B!
Seems super() is hoisted at the beginning of class B, right?

Class fields are all installed as the last step of (or "right after") the super call. This means that [this.name](http://this.name) is overwritten with "class B " as soon as the super call is finished.

Adding more logs reveals the order:

class A {
	name = (console.log("A:1 - init name in A"), "class A ");
	constructor(){
        console.log("A:2 - construct A");
		this.name = this.name + "A!";
	}
}

class B extends A{
   name = (console.log("B:2 - init name in B"), "class B ");
   constructor(){
    console.log("B:1 - construct B");
    try {
      this;
    } catch (e) {
      console.error(e);
    }
    super();
    console.log("B:3 - post super()");
   	this.name = this.name + "B!";
   }
}

Gives:

new B();

> [Log] B:1 - construct B
> [Error] ReferenceError: 'super()' must be called before accessing |this| or returning non-object.
> [Log] A:1 - init name in A
> [Log] A:2 - construct A
> [Log] B:2 - init name in B
> [Log] B:3 - post super()

Hmmm unrelated but isnt Class A in OP missing a super call? :thinking: Thought you couldn't access this without it. Or is it implicit because super in class B is called?

No, base classes don't need (and can't have) super().

1 Like
var i = 1;
class A{
	name = console.log("Step " + i++ + ": set fields of A");
	constructor(){
		console.log("Step " + i++ + ": run A.constructor()");
	}
}

class B extends A{
	name = console.log("Step " + i++ + ": set fields of B");
	constructor(){
		console.log("Step " + i++ + ": run statements before super() at B.constructor");
		super();
		console.log("Step " + i++ + ": run statements after super() at B.constructor");
	}
}

new B();

result:

Step 1: run statements before super() at B.constructor
Step 2: set fields of A
Step 3: run A.constructor()
Step 4: set fields of B
Step 5: run statements after super() at B.constructor
1 Like

May I ask why it behaves this way? I would have expected the same behavior that the O.P. expected, do we know the reasons behind these design choices?

I can't understand that expectation. The design choice here was whether to use [[Define]] or [[Set]], but in either case here, name = 'Class B '; would have been the same as [this.name](http://this.name) = 'Class B'; in the constructor, right after super().

Can you help me understand why you'd expect the public field to do something different?

I guess the natural expectation I would intuitively have is that these class fields execute before any of the constructors run in the prototype chain, so that the fields are fully initialized before any constructor logic begins to execute. But, I guess what I'm describing is probably not possible to do, or easy to implement in JavaScript due to how it's built on this dynamic, prototypal model. I just don't find this current behavior very intuitive (but it might have been the only realistic option) - adding a super() call to a constructor is, without warning, causing these class field initializations to be delayed until after the super() call happens, instead of before.

Class fields are basically syntactic sugar over initialising properties in the constructor. They don't get any special treatment by new (i.e. [[construct]]).

It's not like you can just "add" that to any constructor. You have to add it when your class extends another class, and you must not add it if it doesn't.
It's just like the rule that you cannot use this before the super() call.

Yeah, that's also an unintuitive rule for me. I've known it's existed, but have never put much thought into it, as I've just assumed that it's also done due to limitations in how inheritance is built on top of the dynamic prototypal model. I guess it wouldn't be possible to initialize the fields before any constructor runs if the "this" object hasn't even been created yet. And, since a constructor can return any object as the new instance when you "new" it (it doesn't have to return the "this" value), it makes sense that a "this" object can not exist until super() is called, so it seems the way it's currently implemented would be the only valid way to do so within the JavaScript language.

If helpful, the way I view this is that the classes are effectively constructed in reverse. So new B constructs A and then B makes its changes.
All the subclasses can really do first is decide what arguments to pass down the constructor chain.

So from A’s perspective the only difference when it is constructed directly vs as a subclass is the prototype of this. Nothing else about this will be any different, this potentially makes classes easier to reason about.

2 Likes