Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Super call vs super property inconsistency #1654

Open
bakkot opened this issue Aug 5, 2019 · 6 comments

Comments

@bakkot
Copy link
Contributor

commented Aug 5, 2019

Consider the following code:

class A {
  constructor() {
    console.log('A supercall');
  }
  printName() {
    console.log('A method');
  }
}
class B {
  constructor() {
    console.log('B supercall');
  }
  printName() {
    console.log('B method');
  }
}
class C extends A {
  constructor() {
    super();
    super.printName();
  }
}

Object.setPrototypeOf(C, B);
new C(); // prints `B supercall`, `A method`

In this code

  • the super() prints B supercall, because super() looks up the current prototype of C (in GetSuperConstructor), which is B,
  • whereas the super.printName() prints A method, because super.printName looks up the [[HomeObject]] of C (in MakeSuperPropertyReference), which is its original prototype A.

That's... weird. Is this intentional? If it is a bug, is it conceivable that it could be fixed without breaking anyone? (I'm guessing not, which is unfortunate.)

@allenwb

This comment has been minimized.

Copy link
Member

commented Aug 5, 2019

It is working exactly as per the specification.

You manually "rewired" the [[Prototype]] of the constructor C but you didn't rewire the [[Prototype]] of C.prototype. You made constructor C inherit from constructor B but left C.prototype inheriting from A.prototype.

If a programmer is going to rewire this or other aspects of the objects created by a class definition it is up to them to know what they are doing and to get it right (or at least get it the way they want it). If they mess it up, the spec. stills defines what should happen in all implementations.

@allenwb

This comment has been minimized.

Copy link
Member

commented Aug 5, 2019

BTW, you could imagine a new function, Object.setSuperclassOf, that did the "right thing" but I suspect it would be an attractive nuisance.

@bakkot

This comment has been minimized.

Copy link
Contributor Author

commented Aug 5, 2019

@allenwb, my question is why the spec chooses this particular behavior, such that super.foo is based on the [[HomeObject]] but super() is not.

@allenwb

This comment has been minimized.

Copy link
Member

commented Aug 6, 2019

super() (a super constructor call) and super.foo (a super property access) are completely different operations. super() directly invokes the [[Construct]] internal of the object that is the [[Prototype]] value of the constructor function that invokes super(). super() does not perform a property lookup.

super.foo is a property lookup. For instance methods of a class definition, C, the property lookup starts at the value of the [[Prototype]] property of the original value of C.prototype (this is the value of the methods [[HomeObject]] internal slot

For static methods of C, super.foo performs a property lookup starting at the object that is the value of C's [[Prototype]]. The [[HomeObject]] of a static method of C is C.

Why does it work this way. Because a super property lookup must start "up" the prototype chain from the object where the current method (the one that contains the super.foo reference) was found. But how does the current method know "where it was found"? Arguably, the most dynamically flexible solution would be on each method invocation to explicitly pass its "Home" (where it was found) as an additional argument. (Imagine, that an additional argument is added to the [[Call]] internal method.) But a method call site doesn't know anything about the actual function it is calling--it doesn't know whether or not the called function actually performs a super property accesses. So, the additional "home" argument would have to be passed on all [[Calls]]s even though only a tiny fraction of all method calls actually needed. If you assume that the typical method call has approximately one argument in addition to the this argument, adding a "home" argument to every call would be a 50% increase in the argument passing overhead for each call. TC39 was unwilling to consider imposing that overhead on every call.

The chosen alternative (the [[HomeObject]] binding) is a compromise solution. It eliminates the need to explicitly pass the "home" on every calls. But the price paid is that each method that contains a super property reference is statically bound to the object (typically a class constructor or a class prototype object, but also object literals). Super property access still works as expected in all normal situations. It even works as expected if someone manually changes the [[Prototype]] of a [[HomeObject]] value. But it will behave surprisingly in one situation.

If somebody extracts a method with a [[HomeObject]] binding from its home object, installs that method in some other object, and then invokes that method on the other object, the super property access will follow the prototype change of the original home object rather than its new "home". ES6 was originally going to include a way to install a copy of a method with a new [[HomeObject]] binding into a new home. But TC39 decided to drop/defer that functionality because of complexity/low utility/unknown exploitably concerns.

@bakkot

This comment has been minimized.

Copy link
Contributor Author

commented Aug 6, 2019

I understand that super() and super.foo are different operations, but since they both use the word super, it seems surprising that they can refer to unrelated things within a single method invocation, rather than meaning, for example, "the class's current prototype" and "the .prototype property of the class's current prototype". And there's no obvious-to-me reason for this discrepancy.

@allenwb

This comment has been minimized.

Copy link
Member

commented Aug 6, 2019

super is never used as a standalone token. super() is only allowed within a class constructor. It always means " invoke the [[Construct]] internal method of the current value of the constructor's [[Prototype]] internal slot". super.foo and `super["foo"] always means use the [[HomeObject]] of the current method to start a "prototype" property lookup.

Note that use of a super to implicitly do a property lookup using the current method name (whatever that really would mean) was not allowed even though some people wanted it.

Both uses of super as currently defined fulfill the expectation of most developers who are familiar with similar syntax in other OO languages, particular that ones that have explicit constructors. A more distinguishing syntax (perhaps new.super() might have been used but it would have been unfamiliar to everyone.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
2 participants
You can’t perform that action at this time.