3

I'm trying to figure out how to call a constructor function with an arbitrary number of arguments (passing the arguments on from some other function call).

I have a set of base and derived objects in javascript. One of the methods on the base object is called makeNew() and its job is create a new object of the same type as whatever object it's called on and process all the same arguments as the normal constructor would on the new object. The point of this makeNew() method is that there are other methods that want to create a new object of the same type as the current object, but they won't know what type that is because it may be a type that inherits from the base class. Note, I don't want a whole clone of the current object, but rather a new object of the same type, but initialized with different initial arguments to the constructor.

The simple version of makeNew() is this:

set.prototype.makeNew = function() {
    return new this.constructor();
}

This works for creating an empty object of the same type as the current one because if it's an inherited object type, then this.constructor will be the inherited constructor and it will make the right type of object.

But, when I want to pass on arbitrary arguments to the constructor so normal constructor arguments can be passed to makeNew(), I can't figure out how to do it. I tried this:

set.prototype.makeNew = function() {
    return new this.constructor.apply(this, arguments);
}

But, that code gives me this error in Chrome:

Error: function apply() { [native code] } is not a constructor

Any idea how to pass on arbitrary arguments to a constructor function?

jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • I didn't understand what was the problem with the first version. `return new this.constructor(arguments);` should work. – TastySpaceApple Feb 24 '14 at 19:58
  • 1
    @TastySpaceApple - that passes a single argument to the constructor that is a single array-like object which is not how the constructor expects its multiple arguments to be passed. – jfriend00 Feb 24 '14 at 20:10
  • @cookiemonster - I'll have to try that technique in the post you referenced. That's an interesting hack. – jfriend00 Feb 24 '14 at 20:28
  • 1
    If you mean the ones with the `F` temporary constructor, that's basically the `Object.create()` (partial) shim. – cookie monster Feb 24 '14 at 20:30
  • @cookiemonster - one thing I don't like about the link you posted is that it leaves you with an object that's got a name "F" which doesn't actually cause programming problems on first glance, but creates an extra level of inheritance over what an object created normally would have. It is still a hack, not a fully clean solution. FYI, I implemented a test of it here: http://jsfiddle.net/jfriend00/rXyXJ/ – jfriend00 Feb 24 '14 at 21:08
  • @jfriend00: Yes, there's an extra object in the prototype chain. The only perfectly clean solution would be on that the language supports, which doesn't exist until ECMAScript 6. If you don't consider `eval` to be unclean, then here's an alternate that gives arbitrary length and maintains type: http://jsfiddle.net/7t35t/ – cookie monster Feb 24 '14 at 21:15
  • @cookiemonster - I'm not a fan of `eval()` (e.g. strict mode), but doesn't that example only work for string arguments? – jfriend00 Feb 24 '14 at 21:19
  • @jfriend00: No, the only thing that's eval'd is the function parameters. The actual arguments are passed to the function after its creation. It's just a custom made function with the exact number of defined parameters for that specific call. For example, if the `coll` has `5` members, it becomes `function(a,b,c,d,e) { return new ctor(a,b,c,d,e); }` – cookie monster Feb 24 '14 at 21:22
  • ...shouldn't be any strict mode issues, though I'm not crazy about `eval()` either. Updated demo with Array arguments and strict mode. http://jsfiddle.net/7t35t/1/ – cookie monster Feb 24 '14 at 21:24
  • I've certainly marked my share of questions as duplicates, but in this case, the better answer for my situation is not in that duplicate, but is what I discovered through this discussion and wrote up as an answer to my own question. There are sometimes reasons for having new discussion among new people for a new situation. – jfriend00 Feb 24 '14 at 21:29

3 Answers3

3

The only way I see it is that you'll have to create a method in-between to spread the array into arguments.

This method, for example, first creates the new object, and then uses apply to spread the array.

Function.prototype.construct = function(argArray) {
    var constr = this;
    var inst = Object.create(constr.prototype); //creates a new empty object
    constr.apply(inst, argArray); // triggers the constructor (as a function)
    return inst;
};

then you can use

return set.construct(array)

Read more (much more) at http://www.2ality.com/2011/08/spreading.html

Note: Object.create might now work on old browser. Use the polyfill from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create

TastySpaceApple
  • 3,115
  • 1
  • 18
  • 27
  • This is the way to do it. There are some other hackish ways, but this is cleanest. – cookie monster Feb 24 '14 at 20:20
  • Thx. I had seen something like this before. I was hoping not to depend upon `Object.create()` so older IE 8 could still run my code. – jfriend00 Feb 24 '14 at 20:22
  • Sham it? Would that still work? – Xotic750 Feb 24 '14 at 20:23
  • You could add the polyfill from https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/create/ and that would provide support for older browsers.. – TastySpaceApple Feb 24 '14 at 20:27
  • 1
    @Xotic750: Sure, this technique doesn't rely on any behavior that can't be shimmed. – cookie monster Feb 24 '14 at 20:27
  • Yes, it does indeed seem to work with the sham, though slightly different to the code posted. `var constr = this.constructor,` http://jsfiddle.net/Xotic750/Mg6U5/ For some reason my brain was nagging that it wouldn't :) – Xotic750 Feb 24 '14 at 20:37
1

If you can change the constructor, this way seems legit to me, it relies on the delicate differences between constructors, objects, and functions in javascript.

function set(arg1, arg2, arg3){
    // entered the constructor, `this` = new object (like using Object.create)
    if(arg1 instanceof Array){ //if arg1 is an array
        this.constructor.apply(this, arg1); //use apply to spread it
        // apply now refers to the constructor as a function
        // notice the reference to `this.constructor`. 
        // This is because `this` no longer refers to the function, 
        // but to the newly created object.
    }else{
        // the actual function.
        console.log(arg2);
    }
}
new set([1,2,3])

Now it's up to you to figure out how to make this work in prototypical inheritance... They way I see it, you'll have to copy this code in to every class. Or have it inherited:

function set(arg1, arg2, arg3){
    if(set.prototype.initCheck.call(this, arg1)){ //this will have to be copied to every inherited class...
        ///the actual constructor
    }
}
set.prototype.initCheck = function(arg1){
    if(arg1 instanceof Array){ //if arg1 is an array
        this.constructor.apply(this, arg1); //use apply to spread it
        return false;
    }
    return true;
}
new set([1,2,3])
TastySpaceApple
  • 3,115
  • 1
  • 18
  • 27
  • 1
    Actually you remind me of one of the reasons that you might choose to put no initialization code in the constructor itself, but rather in an `init()` method that the constructor calls. This way you don't have to call the constructor with the arguments. You can call a blank constructor, get a new empty object and then call `this.init.apply(this, arguments)` on the new object to get the exact same result as calling the constructor with the arguments. It's a convention, but following that convention solves this issue (if you control all the objects involved). – jfriend00 Feb 24 '14 at 21:04
  • Huh, that's a nice idea. – TastySpaceApple Feb 24 '14 at 21:19
0

Supplying an answer to my own question. The various discussion led me to a couple possible solution to this.

If you control all the objects involved, you can take all the active code out of the constructors and put it in an .init() method and follow that design pattern. Then, you can do this:

function set() {
    this.init.apply(arguments);
}

set.prototype.init = function() {
    // process all constructor arguments here
}

set.prototype.makeNew = function() {
    // create object of the same type as what we are
    // even if it's a derived object type
    var newSet = new this.constructor()

    // apply any constructor arguments
    newSet.init.apply(newSet, arguments);

    return newSet;
}

All derived classes would also have to follow the same .init() design pattern including calling the base class if they added or processed any constructor arguments themselves beyond just letting the base class handle the constructor arguments.


Actually, in cases where an empty constructor doesn't do much and doesn't have any issues being called twice, you can even do this:

function set() {
    // do whatever object initialization from arguments
    // that the object wants to do
}

set.prototype.makeNew = function() {
    // create object of the same type as what we are
    // even if it's a derived object type
    // first time called without any constructor arguments
    var newSet = new this.constructor();

    if (arguments.length) {
        // apply any constructor arguments
        // by simply calling the constructor again without "new"
        // and this time passing it the object we already made
        // and the arguments we have
        this.constructor.apply(newSet, arguments);
    }

    return newSet;
}
jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • Would be specific to constructors that have no bad side-effects to being called with no args, but for that case it should work fine. – cookie monster Feb 24 '14 at 21:20
  • @cookiemonster - yes, you're right. I also found an even simpler way if the constructor doesn't mind being called twice first with no args and then with the args. – jfriend00 Feb 24 '14 at 22:18