super in constructor should be distinct (Re: (Weak){Set|Map} subclassing)

# Herby Vojčík (11 years ago)

Allen Wirfs-Brock wrote:

super(...) is just shorthand for super.constructor(...) (when it occurs in a constructor) so it is just a use of [[Call]]. No magic.

[[Call]]/[[Init]] aside.

After reading the spec, it is really so that super(...) in constructor is in fact super.constructor(...), because

  • constructor has MethodName "constructor"
  • super(...) has generic superMethodName semantics. IOW, constructor is compiled as any other method with respect to semantics of its code (it gains its [[Construct]] and .prototype magic later, but these are external and do not touch the code).

It is bad for two reasons:

  1. It will fail. There is lot of examples of code like:

    function Foo() { ... } Foo.prototype = Object.create(Bar.prototype); // or even (!!!) Foo.prototype = new Bar(); Foo.prototype.baz = function () {...}; ...

all over the web, in libraries etc. Some people add "Foo.prototype.constructor = Foo;" there, but some don't. Now you see what happens if I try to use the new class:

class Quux extends Foo { constructor() { ....; super(...); ... } }

super(...) call fails, because there is no Foo.prototype.constructor.

What's additional risk, the .constructor can be writable/configurable, so something may be injected / it may be deleted. It is not what developers assume. This relates to the:

  1. It is not right (imho). Constructors are not methods (is there a movement of making them into ones? I doubt, constructor methods are too in-grained in the ECMAScript structure).

Constructors are not methods, they are much more external to the class. They are the class, so what nearly every developer assumes when calling super(...) in the constructor with extends Foo to call Foo. Not Foo.prototype.constructor, which is way too brittle.

It is the common practice now, Foo.apply(this, arguments) or Foo.call(this, ...). The ES6 super(...) does something else.

Therefore, I propose super(...) has different semantics for constructor methods*. It should do roughly (ctr being the constructor function):

  1. Get [[Prototype]] of ctr into proto, fail if problem.
  2. If proto is constructor function, 2.1. [[Call]]** proto with thisArg of this, and argument list equal to super(...) argument list 2.2. return the completion of 2.1.
  3. Otherwise, throw.***

Allen

Herby

  • That is, I'd like to see it in freeform constructor methods as well. Only with super(...) shortened form. It's semantics is straightforward and prompts replacing Bar.call(this, ...) with saner super(...); of course, one must rewire Foo.proto = Bar; to get this. May help the transition and understanding that class has constructor inheritance as well, not only prototype inheritance. ** Or [[Init]] if adopted. *** Alternatively, revert to super.constructor(...). I don't like it, though.
# Allen Wirfs-Brock (11 years ago)

On Dec 5, 2012, at 5:45 AM, Herby Vojčík wrote:

Allen Wirfs-Brock wrote:

super(...) is just shorthand for super.constructor(...) (when it occurs in a constructor) so it is just a use of [[Call]]. No magic.

[[Call]]/[[Init]] aside.

After reading the spec, it is really so that super(...) in constructor is in fact super.constructor(...), because

  • constructor has MethodName "constructor"
  • super(...) has generic superMethodName semantics. IOW, constructor is compiled as any other method with respect to semantics of its code (it gains its [[Construct]] and .prototype magic later, but these are external and do not touch the code).

It is bad for two reasons:

This is rehashing old territory, but let's go again...

  1. It will fail. There is lot of examples of code like:

function Foo() { ... } Foo.prototype = Object.create(Bar.prototype); // or even (!!!) Foo.prototype = new Bar(); Foo.prototype.baz = function () {...}; ...

all over the web, in libraries etc. Some people add "Foo.prototype.constructor = Foo;" there, but some don't. Now you see what happens if I try to use the new class:

class Quux extends Foo { constructor() { ....; super(...); ... } }

If you are subclassing Foo, then you better have some understanding that Foo is a well-formed "class-like" object. Or, from another perspective, not adding a "constructor" property to Foo.prototype is explicating stating that it does not participating in super delegation of calls to "constructor".

In the past, there was minimal penalty to not maintaining the Foo.prototype to Foo link via the "constructor" property. In ES6, this changes. Fortunately, there is really no reason to use the explicit class wiring pattern because a class definition does exactly the same thing for you.

In any case, you are defining the constructor that contains the super refernce. If you are subclass something that is potentially malformed WRT prototype.constructor, then don't use super. Instead, fall back to doing Foo.call(this,...)

super(...) call fails, because there is no Foo.prototype.constructor.

I't will get an inherited constructor property value, probably the one from Object.prototype

What's additional risk, the .constructor can be writable/configurable, so something may be injected / it may be deleted. It is not what developers assume. This relates to the:

We discussed making the .constructor property of the prototype object created by a class definition non-writable/non-configurable but in the end the consensus was to match what chapter 15 built-ins do, which is writable/configurable. (But note that the prototype property of a class created constructor is non-writable/-non-configurable. On a related note, I'm considering whether built-in @@create methods should be non-writable/non-configurable. I can see arguments for both sides of that choice).

However, in general JS has very mutable objects and if you start mutating basic relationships, things may behave oddly. This relates to all uses of super, not just in constructors.

  1. It is not right (imho). Constructors are not methods (is there a movement of making them into ones? I doubt, constructor methods are too in-grained in the ECMAScript structure).

Well, in ES a method is defined to be a property with a function as its value. So, Quux.prototype.constructor is a method, by that definition.

Constructors are not methods, they are much more external to the class. They are the class, so what nearly every developer assumes when calling super(...) in the constructor with extends Foo to call Foo. Not Foo.prototype.constructor, which is way too brittle.

Note that constructors are invoked by [[Construct]], as if they, were a method. Specifically the method is [[Call]]'ed with the this value set to the new instance. So this.baz() within a constructor function is a method call on an instance. So is this.constructor().

Since super(...) currently doesn't exist in JS, I don't think we can say what it means to nearly every developer other than that currently it means produce a syntax error. In the future, what super(...) should mean, anywhere in ES code, is do a super invoke using the property name of the currently executing method. For constructors, that name is "constructor".

It is the common practice now, Foo.apply(this, arguments) or Foo.call(this, ...). The ES6 super(...) does something else.

It's common ES<6 practice, to do this in any method that needs to approximate super call behavior. But it is not how ES6 super is defined. One specific difference is that ES6 super correctly rebinds if proto is used to change the [[Prototype]] of either the class prototype or the constructor object itself (which is relevant to class-side methods)

Therefore, I propose super(...) has different semantics for constructor methods*. It should do roughly (ctr being the constructor function):

Note you are saying that (only) in constructors, super() and super.constructor() have different semantics then they do in other methods(actually we are talking about unqualified and qualified super references in general) . Or do you only want to allow unqualified super in constructors and to disallow it in ordinary methods. eg, the following would be illegal:

class Quux extends Foo { someMethod() {super()} // syntax error, unqualified super in a non-constructor method} }

  1. Get [[Prototype]] of ctr into proto, fail if problem.
  2. If proto is constructor function, 2.1. [[Call]]** proto with thisArg of this, and argument list equal to super(...) argument list 2.2. return the completion of 2.1.
  3. Otherwise, throw.***

think about potential proto rewiring issues.

Allen

Herby

  • That is, I'd like to see it in freeform constructor methods as well. Only with super(...) shortened form. It's semantics is straightforward and prompts replacing Bar.call(this, ...) with saner super(...); of course, one must rewire Foo.proto = Bar; to get this. May help the transition and understanding that class has constructor inheritance as well, not only prototype inheritance.

I'd be fine with allowing super in regular Function definitions. Others aren't. However, I would not want super in such declaration to make this sort of constructor usage assumption. A free standing function is just as likely to be plugged in as a "method" as it is as an constructor". One advantage of such unqualified super references in function declarations (if we ever allow them) is that they are independent of any name in the source code. When the function is super bound to an object (which is necessary to make super work at all) it picks up the actual property name used for that object. For example,

//hypothetical, doing things that ES6 currently does not allow: function loggerMethod(...args) { console.log("called on ", this); return super(...args)}

Reflect.defineMethod(someInstance, "someMethod", loggerMethod); //logs and calls super.someMethod Reflect.defineMethod(someInstance, "anotherMethod",loggerMethod); //logs and calls super.anotherMethod

** Or [[Init]] if adopted.

Unlikely, @@call + [[Call]] seem just fine.

*** Alternatively, revert to super.constructor(...). I don't like it, though.

There are a lot of design trade-offs involved to incorporate super into ES. The issues you bring up was considered in arriving at the current design. So far, your arguments haven't convinced me that we've made the wrong trade-offs. In fact, it has re-enforced (for me) that we have probably made the right ones. We could go through a process that reexamining it. But consensus is fragile (within TC39 and anywhere else) and at some point we need to be content with what we have and move on to new issues. In this light, I really don't want to get into another round of re-engineering super.

I think you may have a reasonable point about the integrity of the Foo.prototype.constructor relationship established by class definitions. I originally proposed that the constructor property of such prototype objects should be non-writable/non-configurable. However, at the July meeting we decided to make it be consistent with the existing chapter 15 classes which have writable/configurable constructor properties. There is a reasonable argument based both upon internal consistency and flexibility for monkey patching behind that decision. I would still be fine with making them non-writable/non-configurable. But, without that, you still have the freedom to "freeze" Foo,prototype.constructor for your Foo function if you really are worried about the integrity of super references in the constructor. If so you probably are also worried about the integrity of all of your methods, so you might as well freeze Foo.prototype.

# Herby Vojčík (11 years ago)

Allen Wirfs-Brock wrote:

On Dec 5, 2012, at 5:45 AM, Herby Vojčík wrote:

Allen Wirfs-Brock wrote:

super(...) is just shorthand for super.constructor(...) (when it occurs in a constructor) so it is just a use of [[Call]]. No magic. [[Call]]/[[Init]] aside.

It is bad for two reasons:

This is rehashing old territory, but let's go again...

  1. It will fail. There is lot of examples of code like:

function Foo() { ... } Foo.prototype = Object.create(Bar.prototype); // or even (!!!) Foo.prototype = new Bar(); Foo.prototype.baz = function () {...}; ...

all over the web, in libraries etc. Some people add "Foo.prototype.constructor = Foo;" there, but some don't. Now you see what happens if I try to use the new class:

class Quux extends Foo { constructor() { ....; super(...); ... } }

If you are subclassing Foo, then you better have some understanding that Foo is a well-formed "class-like" object. Or, from another

Apart from rewiring constructor, it is. It is instantiated with new, it's .prototype is filled with methods... Just, author did not rewire constructor. Because, it's a burden and no one really needs .constructor anyway.

perspective, not adding a "constructor" property to Foo.prototype is explicating stating that it does not participating in super delegation of calls to "constructor".

I'd argue this is simply not true. The construct was in good faith created as a class-like one. It is just not 100%, one previously really unnecessary invariant wasn't preserved.

In the past, there was minimal penalty to not maintaining the Foo.prototype to Foo link via the "constructor" property. In ES6, this changes. Fortunately, there is really no reason to use the

Kind of breaking change, when taking into account all those "non-classy classes" out there.

explicit class wiring pattern because a class definition does exactly the same thing for you.

I am talking legacy code here.

Maybe at least Reflect.fixClass(Foo) would help to mitigate it (call it before on invalid foreign class, it will rewire if it's not right; I think rewiring both Class.prototype.constructor as well as Class.proto).

But adding API just to help with transition seems a little strange.

In any case, you are defining the constructor that contains the super refernce. If you are subclass something that is potentially malformed WRT prototype.constructor, then don't use super. Instead, fall back to doing Foo.call(this,...)

Hm. It would be better if super(...) could be used in subclassing those legacy classes, too.

super(...) call fails, because there is no Foo.prototype.constructor.

I't will get an inherited constructor property value, probably the one from Object.prototype

Oh. Yes. But not a lot better. Still not what expected. But yes, it won't fail.

What's additional risk, the .constructor can be writable/configurable, so something may be injected / it may be deleted. It is not what developers assume. This relates to the:

We discussed making the .constructor property of the prototype object created by a class definition non-writable/non-configurable but in the end the consensus was to match what chapter 15 built-ins do, which is writable/configurable. (But note that the prototype property of a class created constructor is non-writable/-non-configurable. On a related note, I'm considering whether built-in @@create methods should be non-writable/non-configurable. I can see arguments for both sides of that choice).

Hard one. But rather be on the side dynamic side. You can change the constructor, you can change the (other) methods by manipulating the prototype, even if it in itself is fixed. You should be able to change @@create as well in that case.

However, in general JS has very mutable objects and if you start mutating basic relationships, things may behave oddly. This relates to all uses of super, not just in constructors.

  1. It is not right (imho). Constructors are not methods (is there a movement of making them into ones? I doubt, constructor methods are too in-grained in the ECMAScript structure).

Well, in ES a method is defined to be a property with a function as its value. So, Quux.prototype.constructor is a method, by that definition.

Hm. Too formal. Not every function in an object prototype is a method; it may be just put there as a value which is fetched and used otherwise (as a node.js callback, say).

For me, method is such a function that is meant to be called in the receiver.method(...) way (directly or indirectly via .call and .apply, doesn't matter).

Quux.prototype.constructor is questionable here. Since it is in .prototype, it can be called as instance.constructor(...), but is it meant to be used this way? Some (including me) say "no, it is obviously meant to be called via new Class(...)", other may say "yes, since using this for sane reasons is enough to be a method".

(I don't see Foo.call(this, ...) as a case for method-ness, it's a simple workaround made pattern, a workaround for not having super(...) yet. IMO)

Constructors are not methods, they are much more external to the class. They are the class, so what nearly every developer assumes when calling super(...) in the constructor with extends Foo to call Foo. Not Foo.prototype.constructor, which is way too brittle.

Note that constructors are invoked by [[Construct]], as if they, were a method. Specifically the method is [[Call]]'ed with the this value set to the new instance. So this.baz() within a constructor function is a method call on an instance. So is this.constructor().

Since super(...) currently doesn't exist in JS, I don't think we can say what it means to nearly every developer other than that currently it means produce a syntax error. In the future, what

Well, for case of constructors, we can; imnsho, they think that super(...args) is the same as the current practice of: Foo.apply(this, args).

The case of methods are unqualified super(...) is a little less obvious (will write more below).

super(...) should mean, anywhere in ES code, is do a super invoke using the property name of the currently executing method. For constructors, that name is "constructor".

I see the elegance and DRYness of this approach; I like it from the technical point of view. But the semantics is not quite right for me.

It is the common practice now, Foo.apply(this, arguments) or Foo.call(this, ...). The ES6 super(...) does something else.

It's common ES<6 practice, to do this in any method that needs to approximate super call behavior. But it is not how ES6 super is defined. One specific difference is that ES6 super correctly rebinds if proto is used to change the [[Prototype]] of either the class prototype or the constructor object itself (which is relevant to class-side methods)

This gets strange here. {Sub,Super}Class.prototype is non-conf/non-wr. So you probably mean SubClass.proto and SubClass.prototype.proto. Correct me if I did not understand something.

I understand the "is relevant to class-side methods" is about rewriting SubClass.proto, I see and understand and everything of it is ok, but it is a "plain method call super", eg. not the "inside constructor" scenario. There everything is fine.

When "in constructor", the real clash of our two view is really visible (and everywhere else it's unproblematic). The problem is there are two __proto__s (the ones I mentioned above). You say that when they are changed / out-of-sync / in whatever-else-nonstandard situation, then the authoritative source of super-constructor for the SubClass constructor should be SubClass.prototype.proto.constructor (that is, this.prototype.proto.constructor); and I am saying it should be SubClass.proto itself.

There is absolutely no problem with qualified super call between our views. The only differing question is who is the superconstructor for the SubClass.

So I can counterargument that the existing semantics of super(...) in constructors fail for the case when SubClass.proto is rewired.

Therefore, I propose super(...) has different semantics for constructor methods*. It should do roughly (ctr being the constructor function):

Note you are saying that (only) in constructors, super() and super.constructor() have different semantics then they do in other methods(actually we are talking about unqualified and qualified super references in general) . Or do you only want to allow unqualified super in constructors and to disallow it in ordinary methods. eg, the following would be illegal:

class Quux extends Foo { someMethod() {super()} // syntax error, unqualified super in a non-constructor method} }

Yes, in fact, unqualified calls in plain methods smell a bit for me.

I really understand how they can help with reuse and ease some refactoring scenarios, but I am not content with it. Seems like way too implicit. I'd be better off with only explicit here.

But I feared to open this, it would be too much.

  1. Get [[Prototype]] of ctr into proto, fail if problem.
  2. If proto is constructor function, 2.1. [[Call]]** proto with thisArg of this, and argument list equal to super(...) argument list 2.2. return the completion of 2.1.
  3. Otherwise, throw.***

think about potential proto rewiring issues.

As I said earlier, I in fact care for proto rewiring, just I think for superconstructor the Class.proto, that is, constructor-side inheritance seems to me as the authoritative source.

But I see it may create problems for "extends nonConstructor" classes. Hm.

Allen Herby

  • That is, I'd like to see it in freeform constructor methods as well. Only with super(...) shortened form. It's semantics is straightforward and prompts replacing Bar.call(this, ...) with saner super(...); of course, one must rewire Foo.proto = Bar; to get this. May help the transition and understanding that class has constructor inheritance as well, not only prototype inheritance.

I'd be fine with allowing super in regular Function definitions. Others aren't. However, I would not want super in such declaration to make this sort of constructor usage assumption. A free standing function is just as likely to be plugged in as a "method" as it is as an constructor". One advantage of such unqualified super

When unqualified super would only means "invoke super-constructor", then it won't create clash (could even throw when known not to be called as part of new/super).

references in function declarations (if we ever allow them) is that they are independent of any name in the source code. When the

I understand, but feel bad about it.

function is super bound to an object (which is necessary to make super work at all) it picks up the actual property name used for that object. For example,

//hypothetical, doing things that ES6 currently does not allow: function loggerMethod(...args) { console.log("called on ", this); return super(...args)}

Reflect.defineMethod(someInstance, "someMethod", loggerMethod); //logs and calls super.someMethod Reflect.defineMethod(someInstance, "anotherMethod",loggerMethod); //logs and calls super.anotherMethod

** Or [[Init]] if adopted. Unlikely, @@call + [[Call]] seem just fine. *** Alternatively, revert to super.constructor(...). I don't like it, though.

There are a lot of design trade-offs involved to incorporate super into ES. The issues you bring up was considered in arriving at the current design. So far, your arguments haven't convinced me that we've made the wrong trade-offs. In fact, it has re-enforced (for me) that we have probably made the right ones. We could go through a process that reexamining it. But consensus is fragile (within TC39 and anywhere else) and at some point we need to be content with what we have and move on to new issues. In this light, I really don't want to get into another round of re-engineering super.

Hm. It is understandable.

I think you may have a reasonable point about the integrity of the Foo.prototype.constructor relationship established by class definitions. I originally proposed that the constructor property of such prototype objects should be non-writable/non-configurable. However, at the July meeting we decided to make it be consistent with the existing chapter 15 classes which have writable/configurable constructor properties. There is a reasonable argument based both upon internal consistency and flexibility for monkey patching behind that decision. I would still be fine with making them non-writable/non-configurable. But, without that, you still have the freedom to "freeze" Foo,prototype.constructor for your Foo function if you really are worried about the integrity of super references in the constructor. If so you probably are also worried about the integrity of all of your methods, so you might as well freeze Foo.prototype.

Not really, I see them really a bit differently. Methods are presumed to be patched. But changing a superclass just by changing .constructor in superclass is very strange. I mean, it breaks the invariant, unless I also rewire the SubClass.proto itself. And we're again in chicken and egg.

# Herby Vojčík (11 years ago)

Herby Vojčík wrote:

__proto__s (the ones I mentioned above). You say that when they are changed / out-of-sync / in whatever-else-nonstandard situation, then the authoritative source of super-constructor for the SubClass constructor should be SubClass.prototype.proto.constructor (that is, this.prototype.proto.constructor); and I am saying it should be

Erratum: this.proto.constructor.

# Herby Vojčík (11 years ago)

Herby Vojčík wrote:

Herby Vojčík wrote:

__proto__s (the ones I mentioned above). You say that when they are changed / out-of-sync / in whatever-else-nonstandard situation, then the authoritative source of super-constructor for the SubClass constructor should be SubClass.prototype.proto.constructor (that is, this.prototype.proto.constructor); and I am saying it should be Erratum: this.proto.constructor.

Erratum2: of course, no this...., home class's .prototype must be used. Ignore the "that is, ..." parentheses. I should go to sleep.