6

I have used the following function to create instances of unknown classes for some time:

Kernel.prototype._construct = function (constr, args) {
  function F() {
    constr.apply(this, args); // EXCEPTION!
  }
  F.prototype = constr.prototype;
  return new F();
};

If I use prototypes everything works:

function Person(name, surname) {
  this.name = name;
  this.surname = surname;
}

var person = Kernel._construct(Person, ["name", "surname"]); // WORKS!

However, some people are using my library using ES6 native classes in node v4+:

class Person {
  constructor(name, surname) {
    this.name = name;
    this.surname = surname;
  }
}

var person = Kernel._construct(Person, ["name", surname]); // EXCEPTION!

They are getting an error:

TypeError: Class constructors cannot be invoked without 'new'

I need to be able to invoke the constructor with an unknown number of arguments. Any ideas about how to get around this issue?

Amit
  • 45,440
  • 9
  • 78
  • 110
Remo H. Jansen
  • 23,172
  • 11
  • 70
  • 93

3 Answers3

12

There are various ways you can do that:

  1. Using Function object's methods:

    Kernel.prototype._construct = function (constr, args) {
      return new (Function.prototype.bind.apply(constr, [null].concat(args)));
    };
    

    Here we're applying args as arguments for bind. The goal is to have a function that can be called without arugments so that we can call new x(). bind does this for us, but we need to set it up correctly. The syntax is:

    func.bind(thisArg[, arg1][, args2...])
    // calling the results from the above is like
    // thisArg.func(arg1, arg2...);
    

    We want to use constr as the function to bind, and the items in args as the arguments. We don't care about thisArg. To do that, we need to "convert" the args array to arguments. The apply call does that:

    func.apply(thisArg[, args]);
    // calling the results from the above is like
    // thisArg.func(args[0], args[1]...);
    

    apply is actually calling bind. The first item,[null], is important because we want to call bind where thisArg is null- like this: constr.bind(null, args[0], args[1]...).

  2. Using ES2015 Spread operator:

    Kernel.prototype._construct = function (constr, args) {
      return new constr(...args);
    };
    

    This is much simpler, but there are 2 problems:

    1. It requires ES2015 support, and even the latest Node (v4.2.1) requires a flag for this (--harmony_spreadcalls).
    2. This will generate a syntax error if not supported, and you can't even do that conditionally, other then using dynamic scripting (eval() / new Function(...)) - which is not advised.
  3. Using the Reflect built-in in object.

    Kernel.prototype._construct = function (constr, args) {
        return Reflect.construct(constr, args);
    };
    

    This is also simple, but is even further behind in terms of support (basically you must use babel).

Amit
  • 45,440
  • 9
  • 78
  • 110
  • Thanks for your brilliant answer, option one solved my issue :) – Remo H. Jansen Oct 18 '15 at 20:24
  • It should be noted that 1 will work for native classes only with native `Function.prototype.bind` implementation. If accidentally replaced with polyfill, TypeError will still be thrown (transpiled classes are ok with any implementation). – Estus Flask Jan 01 '16 at 11:42
  • Great answer. Solution 1 is really killer and was only option for me – Karamell Jul 20 '17 at 08:03
2

You can get around this by binding the arguments to the function and then calling new on it:

Kernel.prototype._construct = function (constr, args) {
  var F = constr.bind(null, args);
  return new F();
};
2

Good question, but no, you cannot call constructors without the new keyword in ES6 according to ECMAScript §9.22.

I asked an identical question here: ES6: call class constructor without new keyword

I need to be able to invoke the constructor with an unknown number of arguments. Any ideas about how to get around this issue?

Well this says nothing about (not) using the new keyword, but you can easily use variadic arguments in an ES6 constructor

class Person {
  constructor(name, surname, ...others) {
    this.name = name;
    this.surname = surname;
    this.others = others || [];
  }
}

let p = new Person('OweR', 'ReLoaDeD', 'a', 'b', 'c');
p;
//=> {"name":"OweR","surname":"ReLoaDeD","others":["a","b","c"]}

I suspect this isn't what you're after. But, if you're trying to avoid using new to invoke a class in ES6, it cannot be done.


So, with that said, what are you actually trying to accomplish ?

Community
  • 1
  • 1
Mulan
  • 129,518
  • 31
  • 228
  • 259