11

Is this possible? I am creating a single base factory function to drive factories of different types (but have some similarities) and I want to be able to pass arguments as an array to the base factory which then possibly creates an instance of a new object populating the arguments of the constructor of the relevant class via an array.

In JavaScript it's possible to use an array to call a function with multiple arguments by using the apply method:

namespace.myFunc = function(arg1, arg2) { //do something; }
var result = namespace.myFunc("arg1","arg2");
//this is the same as above:
var r = [ "arg1","arg2" ];
var result = myFunc.apply(namespace, r);

It doesn't seem as if there's anyway to create an instance of an object using apply though, is there?

Something like (this doesn't work):

var instance = new MyClass.apply(namespace, r);
Jason Bunting
  • 58,249
  • 14
  • 102
  • 93
Bjorn
  • 69,215
  • 39
  • 136
  • 164
  • I'm not familiar with the OOP terminology... are you trying to create a new instance of, in essence, namespace.MyClass when MyClass is not in namespace? – Anonymous May 01 '09 at 21:29
  • I am trying to create a new instance of namespace.MyClass, but forget about the namespace which is always the same. I'm only trying to take advantage of being able to pass arguments as an array to the constructor when creating an instance. – Bjorn May 01 '09 at 21:30
  • Ah! That makes it more clear. I still don't know whether apply can be used here -- it might -- but I've read some interesting things about list comprehensions and such in bleeding-edge js from Mozilla. – Anonymous May 01 '09 at 21:32
  • Oh right, Maybe I can use that! This is for a Firefox extension. – Bjorn May 01 '09 at 21:34
  • But I'm not trying to create a new array, I'm trying to turn an array into a list of arguments for a function. – Bjorn May 01 '09 at 21:36

5 Answers5

15

Try this:

var instance = {};
MyClass.apply( instance, r);

All the keyword "new" does is pass in a new object to the constructor which then becomes the this variable inside the constructor function.

Depending upon how the constructor was written, you may have to do this:

var instance = {};
var returned = MyClass.apply( instance, args);
if( returned != null) {
    instance = returned;
}

Update: A comment says this doesn't work if there is a prototype. Try this.

function newApply(class, args) {
    function F() {
        return class.apply(this, args);
    }
    F.prototype = class.prototype;
    return new F();
}

newApply( MyClass, args);
Matthew Crumley
  • 101,441
  • 24
  • 103
  • 129
John
  • 483
  • 5
  • 9
  • That's pretty clever, but it doesn't seem to work. At least not in Firefox, it's giving me some ideas though. Hrmm. – Bjorn May 01 '09 at 21:41
  • FWIW, the simple cases of Array.apply({},[2,4,6]) and Number.apply({},["3"]) seem to work correctly. – Anonymous May 01 '09 at 21:44
  • That won't set the objects prototype to MyClass.prototype though, which could be necessary. – Matthew Crumley May 01 '09 at 21:50
  • If you create an instance of MyClass in the normal manner and then use your newApply to do it, the instances will not look similar and their constructor properties will return different results, which wouldn't be desirable. – Jason Bunting May 02 '09 at 02:03
  • Could you clarify about not setting the right prototype? An instance doesn't usually have a prototype property set, and afaict (new Foo).constructor===Foo.apply({}). I know I'm missing something, but I can't tell what from what's been said so far. – Anonymous May 02 '09 at 02:28
  • (sorry, I meant "===Foo.apply({}).constructor") – Anonymous May 02 '09 at 02:28
  • There's no visible "prototype" (although, Spidermonkey does expose it through __proto__), but there's an invisible [[Prototype]] property that's set to MyClass.prototype. – Matthew Crumley May 02 '09 at 03:29
  • In your newApply function, the 5th line should be "F.prototype = class.prototype;". Also, "class" is reserved for future use in JavaScript, so the parameter name should be something else. – Matthew Crumley May 02 '09 at 14:57
  • @Jason, the constructor property is inherited from the object's prototype, so it will be the same. – Matthew Crumley May 02 '09 at 15:00
  • @Matthew: I tested his newApply function - created a type with the constructor function of that type and then with the newApply and their constructor properties were not the same, so what you are saying couldn't be true. :) – Jason Bunting May 02 '09 at 16:06
  • @Jason, Try it after changing the function to set F.prototype to MyClass.prototype. It should work. – Matthew Crumley May 02 '09 at 16:21
  • 1
    @Matthew: I like it, I like it. Clean and gets the job done. I removed my downvote. – Jason Bunting May 04 '09 at 04:07
  • 1
    @Matthew, Thanks for the edit. After the edit it will keep the same prototype. – John May 04 '09 at 14:22
  • I'm afraid it doesn't work with some nasty classes like this one: function PainInTheClass() { if (!(this instanceof arguments.callee)) { throw new Error('Call me with the "new" operator only!'); } if (!arguments.length) { throw new Error('Call me with at least one argument!'); } this.success = true; return this; } – user123444555621 Sep 25 '09 at 13:11
2

Note that

  • new myClass()
    

    without any arguments may fail, since the constructor function may rely on the existence of arguments.

  • myClass.apply(something, args)
    

    will fail in many cases, especially if called on native classes like Date or Number.

I know that "eval is evil", but in this case you may want to try the following:

function newApply(Cls, args) {
    var argsWrapper = [];
    for (var i = 0; i < args.length; i++) {
        argsWrapper.push('args[' + i + ']');
    }
    eval('var inst = new Cls(' + argsWrapper.join(',') + ');' );
    return inst;
}

Simple as that.

(It works the same as Instance.New in this blog post)

user123444555621
  • 148,182
  • 27
  • 114
  • 126
1

Hacks are hacks are hacks, but perhaps this one is a bit more elegant than some of the others, since calling syntax would be similar to what you want and you wouldn't need to modify the original classes at all:

Function.prototype.build = function(parameterArray) {
    var functionNameResults = (/function (.{1,})\(/).exec(this.toString());
    var constructorName = (functionNameResults && functionNameResults.length > 1) ? functionNameResults[1] : "";
    var builtObject = null;
    if(constructorName != "") {
       var parameterNameValues = {}, parameterNames = [];
       for(var i = 0; i < parameterArray.length; i++) {
         var parameterName = ("p_" + i);
         parameterNameValues[parameterName] = parameterArray[i];
         parameterNames.push(("parameterNameValues." + parameterName));
       }
       builtObject = (new Function("parameterNameValues", "return new " + constructorName + "(" + parameterNames.join(",") + ");"))(parameterNameValues);
    }
    return builtObject;
};

Now you can do either of these to build an object:

var instance1 = MyClass.build(["arg1","arg2"]);
var instance2 = new MyClass("arg1","arg2");

Granted, some may not like modifying the Function object's prototype, so you can do it this way and use it as a function instead:

function build(constructorFunction, parameterArray) {
    var functionNameResults = (/function (.{1,})\(/).exec(constructorFunction.toString());
    var constructorName = (functionNameResults && functionNameResults.length > 1) ? functionNameResults[1] : "";
    var builtObject = null;
    if(constructorName != "") {
       var parameterNameValues = {}, parameterNames = [];
       for(var i = 0; i < parameterArray.length; i++) {
         var parameterName = ("p_" + i);
         parameterNameValues[parameterName] = parameterArray[i];
         parameterNames.push(("parameterNameValues." + parameterName));
       }
       builtObject = (new Function("parameterNameValues", "return new " + constructorName + "(" + parameterNames.join(",") + ");"))(parameterNameValues);
    }
    return builtObject;
};

And then you would call it like so:

var instance1 = build(MyClass, ["arg1","arg2"]);

So, I hope those are useful to someone - they allow you to leave the original constructor functions alone and get what you are after in one simple line of code (unlike the two lines you need for the currently-selected solution/workaround.

Feedback is welcome and appreciated.


UPDATE: One other thing to note - try creating instances of the same type with these different methods and then checking to see if their constructor properties are the same - you may want that to be the case if you ever need to check the type of an object. What I mean is best illustrated by the following code:

function Person(firstName, lastName) {
   this.FirstName = firstName;
   this.LastName = lastName;
}

var p1 = new Person("John", "Doe");
var p2 = Person.build(["Sara", "Lee"]);

var areSameType = (p1.constructor == p2.constructor);

Try that with some of the other hacks and see what happens. Ideally, you want them to be the same type.


CAVEAT: As noted in the comments, this will not work for those constructor functions that are created using anonymous function syntax, i.e.

MyNamespace.SomeClass = function() { /*...*/ };

Unless you create them like this:

MyNamespace.SomeClass = function SomeClass() { /*...*/ };

The solution I provided above may or may not be useful to you, you need to understand exactly what you are doing to arrive at the best solution for your particular needs, and you need to be cognizant of what is going on to make my solution "work." If you don't understand how my solution works, spend time to figure it out.


ALTERNATE SOLUTION: Not one to overlook other options, here is one of the other ways you could skin this cat (with similar caveats to the above approach), this one a little more esoteric:

function partial(func/*, 0..n args */) {
   var args = Array.prototype.slice.call(arguments, 1);
   return function() {
      var allArguments = args.concat(Array.prototype.slice.call(arguments));
      return func.apply(this, allArguments);
   };
}

Function.prototype.build = function(args) {
   var constructor = this;
   for(var i = 0; i < args.length; i++) {
      constructor = partial(constructor, args[i]);
   }
   constructor.prototype = this.prototype;
   var builtObject = new constructor();
   builtObject.constructor = this;
   return builtObject;
};

Enjoy!

Jason Bunting
  • 58,249
  • 14
  • 102
  • 93
  • Wow. I didn't even think that there was a Function constructor and that it took a string as the function body! Only comment I have is that I think your constructorName regexp assumes no spaces between function name and opening ( - which is easy to fix. :P – Bjorn May 02 '09 at 05:38
  • Yeah, there are many mysteries to the language, I suppose. :) As for my regex, you are probably right about it needing tweaking, it's works fine as-is if all you are using it for is Firefox because because when you call toString() on a function, the string it returns, at least from all my testing, has no space between the name and the opening paren - of course, if only we could live in such a simple world, right? Glad I could help! – Jason Bunting May 02 '09 at 06:56
  • 2
    This doesn't work with an un-named classes. eg MyNamespace.MyClass = function(){...}. – Crescent Fresh May 02 '09 at 11:59
  • @apphacker, it's usually better to pretend that it doesn't exist. There's almost always a better option (not in this case though). – Matthew Crumley May 02 '09 at 13:38
  • 1
    You can get the function name directly with constructorFunction.name (except with anonymous functions, but like crescentfresh mentioned, the RegExp won't work either). – Matthew Crumley May 02 '09 at 14:22
  • Matthew, that does NOT work in IE, and I don't know about other browsers; see this discussion: http://stackoverflow.com/questions/332422/how-do-i-get-the-name-of-an-objects-type-in-javascript/332429#332429 – Jason Bunting May 02 '09 at 15:58
  • @crescentfresh - true, but hopefully you are not creating constructor functions in that manner, since they are really anonymous constructor functions that you are assigning to a variable and nothing more. There is a big difference. – Jason Bunting May 02 '09 at 15:59
  • @Matthew: You are correct, using the Function constructor in the manner I have done here may be one of the only useful uses for it - in fact, in 4 years of more-or-less continual JavaScript hacking, I don't think I have used it until now. :) – Jason Bunting May 02 '09 at 16:01
  • By the way, to everyone, you might be interested to know that you can do the following: MyNamespace.Foo = function Foo(x,y,z) { ... }; That way you can still assign functions using that syntax but the constructor will then have a name. – Jason Bunting May 02 '09 at 16:21
  • @Jason: the anonymous syntax for classes has wide adoption by namespaced libraries. Secondly, the method of parsing out the function name from a function's toString() method is widely frowned upon, for reason number one. Lastly, MyNamespace.Foo = function Foo(x,y,z) { ... }, although perfectly legal syntax, does not give you a function with the name Foo. Not even from within the class can you use the name "Foo". Sorry to burst that bubble. – Crescent Fresh May 03 '09 at 13:23
  • @crescentfresh: First off, I couldn't care less how other libraries do things - there are no rules. The original poster wants to do something and I am showing him how. Second, the syntax I suggest is not only legal but does work as I outlined, so I don't know what bubble you think you are bursting. Show me how it doesn't work, because I tested it myself to my satisfaction. – Jason Bunting May 03 '09 at 18:45
  • @crescentfresh: By the way, I love the whole "widely frowned upon" tone of your post - by whom? Where? Post some references. And even if you do, here is the thing: I don't care. :) Sorry to burst your bubble of thinking that I care; the JavaScript world is so full of hacks already, I don't care if you don't happen to like mine or not. – Jason Bunting May 03 '09 at 18:47
  • @crescentfresh: You said "Lastly, MyNamespace.Foo = function Foo(x,y,z) { ... }, although perfectly legal syntax, does not give you a function with the name Foo. Not even from within the class can you use the name "Foo". Sorry to burst that bubble." I would like to understand what you are talking about, because this seems to work just fine: var MyNamespace = {}; MyNamespace.Foo = function Foo(x,y) { alert(Foo === MyNamespace.Foo); return x + y; }; – Jason Bunting May 03 '09 at 20:27
  • @Jason: thanks for asking what I meant, honestly. I only meant to present a caveat with your answer, not completely tear it down or something. So what I meant was, the syntax does not create a reference usable inside the class definition (prototype). 6 lines of code at http://pastebin.com/d1f776c90 . I'm using FF(3) and Chrome if that matters. What are you using? – Crescent Fresh May 03 '09 at 21:40
  • So as not to lose track of the original poster's problem (which btw I am of the opinion is impossible to solve for all cases), I can't get Function.prototype.build to resolve the reference either (using the "var A = function B(){}" syntax I mean): http://pastebin.com/m52d280ee – Crescent Fresh May 03 '09 at 21:57
  • @crescentfresh: Well, I went and modified the code at that pastebin location (I didn't know that service existed, that's kind of nice) so that it does work. As long as you fully-qualify the function, you are good to go. Granted, it's extra work, but again, in order to get JavaScript to be really productive, you have to hack it regardless. :) By the way, I use FF 3 and IE 7 and Chrome, but most of the time I spend in FF, as IE is just too painful to use (I have to develop against it at work). – Jason Bunting May 04 '09 at 03:42
  • @crescentfresh: As for the second example you point out, it's true that the namespace causes trouble - my solution could be modified, I believe, to factor that in. Again, hacks and hacks are hacks. :) – Jason Bunting May 04 '09 at 03:50
  • I still want to know what's wrong with John's solution (after I fixed it). – Matthew Crumley May 04 '09 at 03:54
  • ALL: For the fun of it, I added an alternate solution using a partially-applied function...Just having a little fun now. – Jason Bunting May 04 '09 at 04:00
  • @Matthew: Ah, I totally missed the fact that you updated it. Looks good to me, and it is much simpler than my solutions. Maybe @crescentfresh can take a stab at it and make sure it passes his more stringent tests, I don't have time to do the same at the moment, it's time for bed. This has been a good discussion, thanks for all the fish! – Jason Bunting May 04 '09 at 04:06
0

what about a workaround?

function MyClass(arg1, arg2) {

    this.init = function(arg1, arg2){
        //if(arg1 and arg2 not null) do stuff with args
    }

    init(arg1, arg2);
}

So how you can:

var obj = new MyClass();
obj.apply(obj, args);
Johan Öbrink
  • 1,241
  • 1
  • 11
  • 17
  • So, not only do you have to modify existing constructor functions, but now it takes two lines of code to handle this. Not the greatest solution in the world...not even decent, IMNSHO. :) – Jason Bunting May 02 '09 at 01:59
0

One possibility is to make the constructor work as a normal function call.

function MyClass(arg1, arg2) {
    if (!(this instanceof MyClass)) {
        return new MyClass(arg1, arg2);
    }

    // normal constructor here
}

The condition on the if statement will be true if you call MyClass as a normal function (including with call/apply as long as the this argument is not a MyClass object).

Now all of these are equivalent:

new MyClass(arg1, arg2);
MyClass(arg1, arg2);
MyClass.call(null, arg1, arg2);
MyClass.apply(null, [arg1, arg2]);
Matthew Crumley
  • 101,441
  • 24
  • 103
  • 129
  • 1
    I was hoping to avoid requiring changes to classes the factory is instantiating, but maybe I'll have to. – Bjorn May 01 '09 at 22:01
  • Having to change the original constructor functions is a poor way of solving this particular problem. – Jason Bunting May 02 '09 at 02:19
  • I don't know if I would recommend changing the constructor just for this purpose, but it can have other benefits depending on how the class is used, like being able to convert array elements with [].map (similar to how the built-in Number, Boolean, and String constructors work). – Matthew Crumley May 02 '09 at 03:36