0

I am making a library where I would like to provide a helper to extend the base class so that users can extend it even if they are not using ES2015 themselves. The issue is that if the base class happens to be in ES2015 (because it isn't sent through transpiler), my calling of parent constructor from subclass fails with:

Class constructor Foo cannot be invoked without 'new'

Example of what I am trying to achieve:

class Foo {
  constructor() {
    console.log("In Foo");
  }

  superMethod() {
    return console.log("In super method");
  }

  static classMethod() {
    return console.log("In class method");
  }
}

Foo.extendClass = function (constructor, methods) {
  const currentClass = this;
  if (typeof constructor !== 'function') {
    methods = constructor;
    constructor = function () {
      return Object.getPrototypeOf(constructor).apply(this, arguments);
    };
  }
  constructor.prototype = Object.create(currentClass.prototype, {
    constructor: {
      value: constructor,
      writable: true,
      configurable: true
    }
  });
  Object.setPrototypeOf(constructor, currentClass);
  Object.assign(constructor.prototype, methods);
  return constructor;
};

const Bar = Foo.extendClass({
  subMethod: function() {
    return console.log("In sub method");
  }
});

b = new Bar();

The problem is in this line:

return Object.getPrototypeOf(constructor).apply(this, arguments);

So how can I call into parent's constructor? I thought ES2015 are just a sugar on top of standard prototype inheritance, but it looks like you cannot simulate it? Is there another way to define subclasses at run time?

Rojo
  • 2,749
  • 1
  • 13
  • 34
Mitar
  • 6,756
  • 5
  • 54
  • 86

1 Answers1

1

ES6 created a clearer distinction between [[Call]] and [[Construct]] operations. ES6 classes can only be constructed using a [[Construct]] operation, but .call/.apply are [[Call]] operations. To use [[Construct]], you'll need to use Reflect.construct() to create the new instance. e.g.

constructor = function () {
  return Reflect.construct(Object.getPrototypeOf(constructor), arguments);
};

or

constructor = function () {
  return Reflect.construct(currentClass, arguments);
};

I'll also note however that this also ignores another feature new in ES6, which is new.target. You'll also want to preserve that, by doing

constructor = function () {
  return Reflect.construct(currentClass, arguments, new.target);
};

to ensure that the things behave the same way as class syntax would.

Also note that you must not use this inside this function. It is the return value of Reflect.construct that is the new instance, so if you wanted to work with it in your own custom constructor, you'll want to do

constructor = function () {
  const _this = Reflect.construct(currentClass, arguments, new.target);

  _this.foo = "foo";
  return _this;
};

however, if you think about it, this is essentially all the same as doing

constructor = class extends currentClass {}
loganfsmyth
  • 156,129
  • 30
  • 331
  • 251