Maris
Maris

Reputation: 4776

How to call a constructor with dynamic amount of arguments?

I have a constructor and I don't know the number of arguments it needs, for instance:

function someCtor(a,b,c){
   var that = this;
   that.a = a;
   that.b = b;
   that.c = c;
}

I need to create a function which will return the instance of that constructor with a dynamic amount of arguments:

function makeNew(ctor, arguments){
    // this doesn't work, but it shows what I'm trying to achieve
    return new ctor.apply(arguments);
}

I want to use the function to pass the dynamic arguments to the constructor like below:

var instanceOfCtor = makeNew(someCtor, [5,6,7]);

How to implement this function?

Upvotes: 1

Views: 231

Answers (1)

T.J. Crowder
T.J. Crowder

Reputation: 1073978

Note: See the ES2015 compatibility note at the end.

You do it by first creating an object setting its underlying prototype to the object the prototype property on the constructor refers to via Object.create, then calling the constructor via Function#apply:

function makeNew(ctor, arguments){
    var obj = Object.create(ctor.prototype);
    var rv = ctor.apply(obj, arguments);
    return rv && typeof rv === "object" ? rv : obj;
}

Note the bit of fiddling at the end, so we're emulating the new operator correctly: When you call a constructor via new, if it returns a non-null object reference, that ends up being the result of the new expression; if it returns anything else (or nothing), the object created by new is the result. So we emulate that.

Even on pre-ES5 browsers, you can emulate enough of Object.create to do that:

if (!Object.create) {
    Object.create = function(proto, props) {
        if (typeof props !== "undefined") {
            throw new Error("The second argument of Object.create cannot be shimmed.");
        }
        function ctor() { }
        ctor.prototype = proto;
        return new ctor;
    };
}

ES2015 Compatibility Note

If the constructor you're calling was created via ES2015's class syntax, the above won't work, because you can't call ES2015 class constructors that way. For example:

class Example {
    constructor(a, b) {
        this.a = a;
        this.b = b;
    }
}

const e = Object.create(Example.prototype);
Example.apply(e, [1, 2]); // TypeError: Class constructor Example cannot be invoked without 'new' (or similar)

The good news is that will only happen on an ES2015+-compatible JavaScript engine, and only if the constructor was created via class; the bad news is that it can happen.

So how do you make your makeNew bulletproof?

It turns out this is quite easy, because ES2015 also added Reflect.construct, which does exactly what you want makeNew to do but does it in a way that's compatible with both class constructors and function constructors. So you can feature-detect Reflect.construct and use it if it's present (ES2015 JavaScript engine, so a constructor might have been created with class) and fall back to the above if it's not there (pre-ES2015 engine, there won't be any class constructors around):

var makeNew; // `var` because we have to avoid any ES2015+ syntax
if (typeof Reflect === "object" && Reflect && typeof Reflect.construct === "function") {
    // This is an ES2015-compatible JavaScript engine, use `Reflect.construct`
    makeNew = Reflect.construct;
} else {
    makeNew = function makeNew(ctor, arguments){
        var obj = Object.create(ctor.prototype);
        var rv = ctor.apply(obj, arguments);
        return rv && typeof rv === "object" ? rv : obj;
    };
}

That's pure ES5 syntax, so runs on ES5 engines, but uses ES2015's Reflect.construct if it's present.

Upvotes: 5

Related Questions