90

How could I generalise the function below to take N arguments? (Using call or apply?)

Is there a programmatic way to apply arguments to 'new'? I don't want the constructor to be treated like a plain function.

/**
 * This higher level function takes a constructor and arguments
 * and returns a function, which when called will return the 
 * lazily constructed value.
 * 
 * All the arguments, except the first are pased to the constructor.
 * 
 * @param {Function} constructor
 */ 

function conthunktor(Constructor) {
    var args = Array.prototype.slice.call(arguments, 1);
    return function() {
        console.log(args);
        if (args.length === 0) {
            return new Constructor();
        }
        if (args.length === 1) {
            return new Constructor(args[0]);
        }
        if (args.length === 2) {
            return new Constructor(args[0], args[1]);
        }
        if (args.length === 3) {
            return new Constructor(args[0], args[1], args[2]);
        }
        throw("too many arguments");    
    }
}

qUnit test:

test("conthunktorTest", function() {
    function MyConstructor(arg0, arg1) {
        this.arg0 = arg0;
        this.arg1 = arg1;
    }
    MyConstructor.prototype.toString = function() {
        return this.arg0 + " " + this.arg1;
    }

    var thunk = conthunktor(MyConstructor, "hello", "world");
    var my_object = thunk();
    deepEqual(my_object.toString(), "hello world");
});
ROMANIA_engineer
  • 54,432
  • 29
  • 203
  • 199
fadedbee
  • 42,671
  • 44
  • 178
  • 308
  • 1
    Ben Nadel [wrote about this](http://www.bennadel.com/blog/2291-Invoking-A-Native-JavaScript-Constructor-Using-Call-Or-Apply-.htm) extensively. – Eliran Malka Aug 28 '13 at 10:39

7 Answers7

98

This is how you do it:

function applyToConstructor(constructor, argArray) {
    var args = [null].concat(argArray);
    var factoryFunction = constructor.bind.apply(constructor, args);
    return new factoryFunction();
}

var d = applyToConstructor(Date, [2008, 10, 8, 00, 16, 34, 254]);

Call is slightly easier

function callConstructor(constructor) {
    var factoryFunction = constructor.bind.apply(constructor, arguments);
    return new factoryFunction();
}

var d = callConstructor(Date, 2008, 10, 8, 00, 16, 34, 254);

You can use either of these to create factory functions:

var dateFactory = applyToConstructor.bind(null, Date)
var d = dateFactory([2008, 10, 8, 00, 16, 34, 254]);

or

var dateFactory = callConstructor.bind(null, Date)
var d = dateFactory(2008, 10, 8, 00, 16, 34, 254);

It will work with any constructor, not just built-ins or constructors that can double as functions (like Date).

However it does require the Ecmascript 5 .bind function. Shims will probably not work correctly.

A different approach, more in the style of some of the other answers is to create a function version of the built in new. This will not work on all builtins (like Date).

function neu(constructor) {
    // http://www.ecma-international.org/ecma-262/5.1/#sec-13.2.2
    var instance = Object.create(constructor.prototype);
    var result = constructor.apply(instance, Array.prototype.slice.call(arguments, 1));

    // The ECMAScript language types are Undefined, Null, Boolean, String, Number, and Object.
    return (result !== null && typeof result === 'object') ? result : instance;
}

function Person(first, last) {this.first = first;this.last = last};
Person.prototype.hi = function(){console.log(this.first, this.last);};

var p = neu(Person, "Neo", "Anderson");

And now, of course you can do .apply or .call or .bind on neu as normal.

For example:

var personFactory = neu.bind(null, Person);
var d = personFactory("Harry", "Potter");

I feel that the first solution I give is better though, as it doesn't depend on you correctly replicating the semantics of a builtin and it works correctly with builtins.

kybernetikos
  • 8,281
  • 1
  • 46
  • 54
  • 5
    I'm surprised you didn't get any vote for that. Solutions based on creating a separate function and changing its prototype has a disadvantage of changing the `constructor` field whereas combining `bind` with `apply` allows to keep it. – mgol Feb 07 '13 at 19:44
  • This is neat, but it is not supported in IE8 and below. – Dale Anderson Oct 25 '13 at 07:29
  • 2
    Quite right, ie8 is not an ecmascript5 browser (which I do mention). – kybernetikos Oct 25 '13 at 09:09
  • 1
    @kybernetikos Using underscore it's possible to create an ES4 compatible version: http://jsbin.com/xekaxu/1 Feel free to add this to your anwer if you want – Creynders Sep 12 '14 at 07:35
  • @Creynders Unfortunately I don't have a browser available at the moment that is old enough to test it with. Underscore uses the native bind if its available. Did you test it on an actual ie8 or older? – kybernetikos Oct 07 '14 at 22:23
  • awesome! BTW- in the first function, why there's a `NULL` as first element of the array of parameters? What's that `NULL`? – rupps Feb 24 '15 at 00:54
  • 1
    @rupps it's the first argument to bind, which will be the 'this' for the function if it is called in the normal way. As we plan to call it with `new`, it's not particularly relevant, so I set it to null there. There's actually an extra argument in the call example too, but since we have an extra argument (the function itself) at the beginnng of the argument list, it's fine to just reuse that. It does mean that with this call example if you just call the bound function without `new`, the `this` internally will be the function itself, but it saves us creating a new array. – kybernetikos Mar 12 '15 at 07:54
50

Try this:

function conthunktor(Constructor) {
    var args = Array.prototype.slice.call(arguments, 1);
    return function() {

         var Temp = function(){}, // temporary constructor
             inst, ret; // other vars

         // Give the Temp constructor the Constructor's prototype
         Temp.prototype = Constructor.prototype;

         // Create a new instance
         inst = new Temp;

         // Call the original Constructor with the temp
         // instance as its context (i.e. its 'this' value)
         ret = Constructor.apply(inst, args);

         // If an object has been returned then return it otherwise
         // return the original instance.
         // (consistent with behaviour of the new operator)
         return Object(ret) === ret ? ret : inst;

    }
}
James
  • 109,676
  • 31
  • 162
  • 175
  • 1
    Thanks, that works on the test code. Is its behaviour identical to new? (i.e. no nasty gotchas to find.) – fadedbee Jul 29 '10 at 13:05
  • @chrisdew, I've added some comments to the code. – James Jul 29 '10 at 13:11
  • 2
    The behavior is identical to new except for a few oddball functions like `Date`; and proxies (a feature proposed for the next version of ECMAScript, currently only supported in Firefox—you can ignore proxies for now). – Jason Orendorff Dec 12 '11 at 17:06
  • 3
    Nice solution. Just one addition, you could avoid the Temp function and rewrite the 3 first lines using ES5's Object.create: var inst = Object.create(Constructor.prototype); – Xose Lluis Jan 21 '13 at 23:31
  • 1
    It looks like this fails for `XMLHttpRequest` in Chrome (I'm using Version 37.0.2062.94 on OS X 10.9.4) yielding `TypeError: Failed to construct 'XMLHttpRequest': Please use the 'new' operator, this DOM object constructor cannot be called as a function.`. It looks like this is a specific case for `XMLHttpRequest` (and most likely some other objects I'm not aware of). Demo: http://jsfiddle.net/yepygdw9/ – backus Sep 06 '14 at 05:02
  • 1
    Fantastic. I don't suppose anyone has found a way of extending this to allow for more semantic debugging? I tried `Temp.name = Constructor.name` but this is illegal (`name`) is read only. Currently debugging is extremely difficult because everything is `Temp`, and I have to query instances' `__proto__` to find out what they actually are. – Barney Sep 11 '14 at 11:48
  • Barney, try `Temp.prototype.constructor = Constructor;` if Constructor is a named function. If not, you can do `Temp.prototype.constructor = function ClassNameYouWant(){};`. Class names are derived from the name property of Function.prototype.constructor. (constructor isn't executed when you create a new class - as far as I know it just seems to be there for the deriving instance names - anyone know more details on this?) – Charles Grunwald Dec 22 '14 at 02:10
  • In ES6, this is thankfully easier: `new Constructor(...args)` – Claudia Mar 18 '15 at 02:50
  • This line really confuses me: `inst = new Temp;` Why isn't it `inst = new Temp();` ? – temporary_user_name Apr 01 '15 at 23:41
15

This function is identical to new in all cases. It will probably be significantly slower than 999’s answer, though, so use it only if you really need it.

function applyConstructor(ctor, args) {
    var a = [];
    for (var i = 0; i < args.length; i++)
        a[i] = 'args[' + i + ']';
    return eval('new ctor(' + a.join() + ')');
}

UPDATE: Once ES6 support is widespread, you'll be able to write this:

function applyConstructor(ctor, args) {
    return new ctor(...args);
}

...but you won't need to, because the standard library function Reflect.construct() does exactly what you're looking for!

Jason Orendorff
  • 42,793
  • 6
  • 62
  • 96
6

In ECMAScript 6, you can use the spread operator to apply a constructor with the new keyword to an array of arguments:

var dateFields = [2014, 09, 20, 19, 31, 59, 999];
var date = new Date(...dateFields);
console.log(date);  // Date 2014-10-20T15:01:59.999Z
Ghasem Kiani
  • 129
  • 2
  • 3
4

Another approach, which requires to modify the actual constructor being called, but seems cleaner to me than using eval() or introducing a new dummy function in the construction chain... Keep your conthunktor function like

function conthunktor(Constructor) {
  // Call the constructor
  return Constructor.apply(null, Array.prototype.slice.call(arguments, 1));
}

And modify the constructors being called...

function MyConstructor(a, b, c) {
  if(!(this instanceof MyConstructor)) {
    return new MyConstructor(a, b, c);
  }
  this.a = a;
  this.b = b;
  this.c = c;
  // The rest of your constructor...
}

So you can try:

var myInstance = conthunktor(MyConstructor, 1, 2, 3);

var sum = myInstance.a + myInstance.b + myInstance.c; // sum is 6
  • This is the best for me, clean as hell and elegant solution. – Delta Jul 22 '13 at 05:01
  • The `this instanceof Constructor` check is very cool, but it prevents constructor composition (ie extensible constructors): `function Foo(){}; function Bar(){ Foo.call(this); }` – Barney Sep 10 '14 at 15:32
  • @Barney If `Bar.prototype` = `Foo`, the `instanceof` check should work. – 1j01 Aug 17 '15 at 01:55
3

Using a temporary constructor seems to be the best solution if Object.create is not available.

If Object.create is available, then using it is a much better option. On Node.js, using Object.create results in much faster code. Here's an example of how Object.create can be used:

function applyToConstructor(ctor, args) {
    var new_obj = Object.create(ctor.prototype);
    var ctor_ret = ctor.apply(new_obj, args);

    // Some constructors return a value; make sure to use it!
    return ctor_ret !== undefined ? ctor_ret: new_obj;
}

(Obviously, the args argument is a list of arguments to apply.)

I had a piece of code that originally used eval to read a piece of data created by another tool. (Yes, eval is evil.) This would instantiate a tree of hundreds to thousands of elements. Basically, the JavaScript engine was responsible for parsing and executing a bunch of new ...(...) expressions. I converted my system to parse a JSON structure, which means I have to have my code determine which constructor to call for each type of object in the tree. When I ran the new code in my test suite, I was surprised to see a dramatic slow down relative to the eval version.

  1. Test suite with eval version: 1 second.
  2. Test suite with JSON version, using temporary constructor: 5 seconds.
  3. Test suite with JSON version, using Object.create: 1 second.

The test suite creates multiple trees. I calculated my applytoConstructor function was called about 125,000 times when the test suite is run.

Louis
  • 146,715
  • 28
  • 274
  • 320
1

There is a rehusable solution for this case. For every Class to you wish to call with apply or call method, you must call before to convertToAllowApply('classNameInString'); the Class must be in the same Scoope o global scoope (I don't try sending ns.className for example...)

There is the code:

function convertToAllowApply(kName){
    var n = '\n', t = '\t';
    var scrit = 
        'var oldKlass = ' + kName + ';' + n +
        kName + '.prototype.__Creates__ = oldKlass;' + n +

        kName + ' = function(){' + n +
            t + 'if(!(this instanceof ' + kName + ')){'+ n +
                t + t + 'obj = new ' + kName + ';'+ n +
                t + t + kName + '.prototype.__Creates__.apply(obj, arguments);'+ n +
                t + t + 'return obj;' + n +
            t + '}' + n +
        '}' + n +
        kName + '.prototype = oldKlass.prototype;';

    var convert = new Function(scrit);

    convert();
}

// USE CASE:

myKlass = function(){
    this.data = Array.prototype.slice.call(arguments,0);
    console.log('this: ', this);
}

myKlass.prototype.prop = 'myName is myKlass';
myKlass.prototype.method = function(){
    console.log(this);
}

convertToAllowApply('myKlass');

var t1 = myKlass.apply(null, [1,2,3]);
console.log('t1 is: ', t1);
danyg
  • 71
  • 3