views:

307

answers:

5

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);
+5  A: 

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);
John
That's pretty clever, but it doesn't seem to work. At least not in Firefox, it's giving me some ideas though. Hrmm.
apphacker
FWIW, the simple cases of Array.apply({},[2,4,6]) and Number.apply({},["3"]) seem to work correctly.
Anonymous
That won't set the objects prototype to MyClass.prototype though, which could be necessary.
Matthew Crumley
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
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
(sorry, I meant "===Foo.apply({}).constructor")
Anonymous
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
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
@Jason, the constructor property is inherited from the object's prototype, so it will be the same.
Matthew Crumley
@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
@Jason, Try it after changing the function to set F.prototype to MyClass.prototype. It should work.
Matthew Crumley
@Matthew: I like it, I like it. Clean and gets the job done. I removed my downvote.
Jason Bunting
@Matthew, Thanks for the edit. After the edit it will keep the same prototype.
John
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;}
Pumbaa80
A: 

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
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
A: 

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
I was hoping to avoid requiring changes to classes the factory is instantiating, but maybe I'll have to.
apphacker
Having to change the original constructor functions is a poor way of solving this particular problem.
Jason Bunting
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
+1  A: 

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
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
apphacker
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
This doesn't work with an un-named classes. eg MyNamespace.MyClass = function(){...}.
Crescent Fresh
@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
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
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
@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
@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
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
@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
@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
@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
@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
@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
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
@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
@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
I still want to know what's wrong with John's solution (after I fixed it).
Matthew Crumley
ALL: For the fun of it, I added an alternate solution using a partially-applied function...Just having a little fun now.
Jason Bunting
@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
+1  A: 

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)

Pumbaa80