Why 2 different default constructors?

Why was class designed to need 2 different default constructors? It seems like it would've made things cleaner for there to never be a [[ConstructorKind]] , for all actions to happen as if "derived" were the only kind, for the class base to always be either Object , null , or user specified, and for super() to be an internal function that automatically handles the special case of a null base. So why split the situation into something that requires 2 different constructor types?

As for wanting to remove the need to call super(), that could still be handled fairly easily without needing to manage 2 different forms for the default constructor.

Can you elaborate on how you would handle that in a cleaner way than just having an internal slot like [[ConstructorKind]] with two possible values?

Indeed, the need to call super() is precisely the difference between derived and base constructors that matters. The rest is just technicalities that most users should ignore unless/until they do something very special with prototype chains.

There's more to it than just having [[ConstructorKind]]. There's also the need to emit one of 2 different constructors in accordance with that kind, the tracking of the association between super and the parent constructor, etc... There's a lot of moving parts. However, if you look at 14.6.13 step 5 & 6.e, you see that there is always a need to track the parent constructor, even if it's specified extends null, or not at all.

Given that, first eliminate the "not at all" case by re-spelling it as extends Object explicitly when its not specified. This is almost identical to what is currently done except that the "not at all" case does something pointlessly special here by setting the [[ConstructorKind]] to "base" and forcing the emission of a super()-less constructor. This leaves extends null as the only special case.

Since step 6.e.ii prescribes that for this last case, the constructorParent is %FunctionPrototype%, there's always a function for super() to call. The only problem is that %FunctionPrototype% doesn't have a [[Construct]]. This is where super gets modified. If super were instead an internal function with this kind of behavior, the problem would be solved for all cases.

  1. Let envRec be GetThisEnvironment().
  2. Assert: envRec is a function Environment Record.
  3. Let activeFunction be envRec.[[FunctionObject]].
  4. Assert: activeFunction is an ECMAScript function object.
  5. Let superConstructor be ! activeFunction.[[GetPrototypeOf]]().
  6. If superConstructor is %FunctionPrototype% then
    a. Let retval be an new ECMAScript Object instance.
    b. Let retval.[[Prototype]] be null.
    c. Return retval.
  7. Else if IsConstructor(superConstructor) is false, throw a TypeError exception.
  8. Return ! superConstructor().

Notice that this is only a little more than what GetSuperConstructor already does. The only thing left to tidy up is the pesky determination of whether or not super() is required in user-defined constructors, but that's easy. Just check to see if _superConstructor_ is not _%Object%_ and _superConstructor_.prototype is an ECMAScript Object. If this is true, then require super, otherwise don't.

A bit wordy to be sure, but this get's rid of the null problem for classes and private fields, gets rid of the need to use super even for explicit cases of extends Object (which never made any sense anyway), reduces slot usage, and gets rid of the need to have 2 default constructors based on whether or not there is an extends clause in the ClassHeritage.

What do you think?

I will note that super does mutate the this value (and is the only construct that can do it). And it's by design that you can call it in arrow functions.

Quick question: if you remove super, how do you plan to address passing parameters to the parent constructor? (This is a very common need among Promise subclasses, for instance.)

My original comment about "wanting to remove the need to call super() " was in reference to the dual-default constructor situation. One of them has super() , the other doesn't. Presumably, this exists because there was a desire to allow a base class constructor to work without needing to call super() while requiring it for derived classes. My goal is to remove the need for dual-default constructors, keeping only the one that contains super() , and modifying super() so that it does something appropriate for the extends null case.

As a side goal, I also want class X and class X extends Object to be equivalent. Initializing the base class is always necessary (even if no parameters are involved), so there's no way to get rid of super() . Nor would I do so even if there was. I just wish TC39 had've chosen an approach that's less Java-like. Having super be both a constructor and a prototype reference with pseudo- this -bound methods is just a little to special for my taste, but that's a moot point now.

I was wrong when I said:

In fact, there is no need to call super(), ever, since you can return a custom object instead of using the default one set to the this-binding.

The true difference between derived and base constructors is: whether an object of the appropriate type is initialised (and assigned to the this-binding) before the body of the constructor is interpreted.

The design process of the class construct has been long and bumpy, with trials-and-errors. The final design, which is indeed not conceptually the simplest one, satisfies several constraints that were difficult to reconcile:

  • It grandfathers the prehistoric way to define constructors, namely as: function Foo(x) { /* ... */ }; var foo = new Foo(x). That is to say, the new operator does not change semantics for those constructors, and they may be used as target for the extends clause.

  • It allows to subclass builtin classes (e.g., Array) that don’t behave as plain objects, and that need to be created and initialised in one step.

While true enough, it still misses my point. I'm saying there's no need for the class evaluation process to worry over which of 2 different default constructors to use. The suggestions I offered above serve to reduce this down to the default constructor being the version that uses super(), completely disregarding the other version. As for the 2 special cases:

  • solve the "no extends clause" case by treating it as equivalent to extends Object, which isn't too different from what currently happens.
  • solve the extends null case by moving the special handling into super() so it simply returns an ECMAScript object with a null [[Prototype]] if the base class is null.

Do this and there's no longer a need for the default empty constructor.

What's so hard about reconciling the 2 constraint's you listed? They don't conflict and aren't particularly difficult to handle. Keeping step with the "prehistoric" (:rofl:) way of defining constructors only made since given that all the tools necessary were already there. Subclassing built-in classes was the only moderately tricky part as you have to pass-the-buck of creating the actual instance all the way up the chain to the first ancestor before processing any use of this.

So what am I missing, and what does that have to do with the reasoning behind having 2 default constructors?

Are you suggesting that base class constructors should be required to call super() before accessing the receiver?

(obviously this is an academic argument; the differences between base and derived classes won’t/can’t change at this point)

Not if user written. However, there's no particular reason why the default constructor cannot under the conditions I've given.

The default constructor needs to be expressible in JS (as it currently is) - how would you achieve that with your suggestion?

The default constructor would always be expressible in JS. It would be the current derived-type default constructor. What would be different is that super() would have extra behavioral semantics to handle the extends null case.

Can you give me a code example of a constructor that uses this, conditionally calls super() before doing so and according to which conditions, that works for both a base and a derived class?

I'm not sure I understand where you're going with this line of questions. Somehow I think you're misunderstanding something. The changes I described wouldn't alter any of the semantics that a developer will see, excepting the new equivalence of class X extends Object {} being the same as class X, and extends null wouldn't currently be in conflict with private fields. Everything that is currently true about how a developer creates a class would remain the same, including the use of super() in the constructor of derived classes and its absence in base classes.