Reflect.super

Currently we have Reflect.construct which allows creating an instance of a specified class or constructor function, but we can't run a constructor with an existing instance as the this value, since function.apply and function.call cannot be used on class constructors.

I propose adding Reflect.super(target, thisArgument, argumentList[, newTarget]). As such extending a class with a constructor function would look something like this:

class A {
  constructor () {
    this.x = 1;
  }
  get y () {
    return 2;
  }
}

function B () {
  Reflect.super(A, this, [], B);
  this.z = 3;
}
B.prototype = Object.create(A.prototype);

It's very similar to Reflect.apply with the added ability that it works on class constructors and can pass a value for new.target.

It can be polyfilled using Reflect.construct:

Reflect.super = function (target, thisArgument, argumentList, newTarget) {
  Object.assign(
    thisArgument,
    Reflect.construct(target, argumentList, newTarget)
  );
}

Having it built in would be much more desirable since the polyfill has to create an additional instance for each call.

Yes. Since ES6, a constructor is not just a method that could be called on a value to initialise it, it actually does what the name says: it constructs a new object. This is also how a super() call works: it asks the parent class to construct a new instance, and then uses that as the this value for the current constructor to initialise. There is no thisArgument getting passed to the parent constructor.

So Reflect.super doesn't make sense from a naming perspective. Also, Reflect is a (admittedly, badly named) namespace for the default implementations of Proxy traps, and there is no super proxy trap either.

I don't see any reason to do that if you can just use class syntax instead. But if you're absolutely striving for it, you would write

function B() {
  const o = Reflect.construct(A, [], new.target);
  o.z = 3;
  return o;
}
B.prototype = Object.create(A.prototype);

 console.log(new B);

I understand that is the implementation and thus why this cannot be accessed before a call to super in the constructor. It's not quite the full picture though, because super asks the parent class to construct a new instance using the child classes prototype, and new.target values. Which is effectively the same behavior as using ParentClass.apply(this, args) minus passing of new.target and if apply could be used on class constructors.

I was not aware there was a tight coupling of the 2, although looking at the APIs of both now I can definitely see it. I guess that raises the question of if a super handler for Proxy could be useful. If not would there be a more appropriate location?

Indeed there may not be many use cases, but in the cases where you can't use class syntax for whatever reason, Reflect.construct can work, but it introduces two instances. In your example both o and this are created. Unless JS engines are optimized to not create this if it is not used and there is a object as a return value, but I don't think that's the case since it would be hard to statically determine if the return value was an object.

Not if the parent class is a builtin such as Array, Function or RegExp that constructs exotic objects. Sure, it does essentially does Object.setPrototypeOf(…, new.target.prototype), but it's still important that the parent class constructs the object itself. A Reflect.super call cannot do that.

@robbiespeed

class A { ... }
class B  extends A {
   constructor() {
      super()
      ...
   }
}

is roughly the same as

function A () {
   if (!new.target) throw new TypeError();
}
function B () {
   if (!new.target) throw new TypeError();
   let _this = Reflect.construct(A, [], new.target);
   ...
   return _this;
}

The instance object created for the base, has new.target.protoype assigned to it's prototype before the base class constructor even see's the new object. So it's not like using apply at all. The onus to create an instance is passed all the way down through the base constructor to the engine, which creates the object and assigns the prototype. Then that object bubbles all the way back up, being initialized by each constructor from base to most derived along the way.

1 Like