How can I construct an object using an array of va

2019-02-03 10:18发布

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条回答
我欲成王,谁敢阻挡
2楼-- · 2019-02-03 10:19

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!

查看更多
Deceive 欺骗
3楼-- · 2019-02-03 10:20

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)

查看更多
SAY GOODBYE
4楼-- · 2019-02-03 10:26

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]);
查看更多
Explosion°爆炸
5楼-- · 2019-02-03 10:27

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);
查看更多
萌系小妹纸
6楼-- · 2019-02-03 10:34

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);
查看更多
登录 后发表回答