如何使用call或apply调用JavaScript构造函数?

91
我如何将下面的函数概括为接受N个参数?(使用call或apply?)
是否有一种编程方式可以将参数应用于“new”?我不希望构造函数被视为普通函数。
/**
 * 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("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");
});

1
Ben Nadel在他的博客中详细介绍了这个问题。 - Eliran Malka
7个回答

98

以下是操作步骤:

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]);

调用稍微容易一些。

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

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

您可以使用这两种方式创建工厂函数:

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

它可以与任何构造函数一起使用,不仅限于内置构造函数或可用作函数的构造函数(例如 Date)。

但是,它需要 Ecmascript 5 的 .bind 函数。 虽然可能会有 shims,但可能无法正常工作。

另一种更符合其他答案风格的方法是创建内置 new 的函数版本。 这将无法在所有内置对象上运行(如 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");

现在,你当然可以像平常一样对 neu 使用 .apply.call.bind

例如:

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

我认为我的第一个解决方案更好,因为它不依赖于你正确复制内置的语义,并且可以正确地与内置工作。


5
我很惊讶你没有得到任何投票。基于创建单独函数和更改其原型的解决方案存在一个缺点,即改变了“constructor”字段,而结合“bind”和“apply”则可以保持它不变。 - mgol
这很好,但它不支持IE8及以下版本。 - Dale Anderson
2
非常正确,ie8不是一个ECMAScript 5浏览器(这一点我已经提到了)。 - kybernetikos
1
@kybernetikos 使用下划线可以创建一个 ES4 兼容版本:http://jsbin.com/xekaxu/1。如果您愿意,可以将此添加到您的答案中。 - Creynders
1
@rupps 它是绑定(bind)的第一个参数,如果以正常方式调用函数,它将成为该函数的'this'。由于我们计划使用new来调用它,所以它并不特别相关,因此我在那里将其设置为null。实际上,在调用示例中还有一个额外的参数,但由于我们在参数列表的开头有一个额外的参数(函数本身),所以只需重复使用即可。这意味着对于这个调用示例,如果你只是调用绑定的函数而没有使用new,内部的this将是函数本身,但这样可以避免我们创建一个新数组。 - kybernetikos
显示剩余2条评论

50

试一下这个:

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;

    }
}

1
谢谢,它在测试代码上运行正常。 它的行为是否与新的完全相同? (即没有什么不好发现的) - fadedbee
2
行为与新的相同,除了一些奇怪的函数,如Date;和代理(这是ECMAScript下一个版本提出的功能,目前仅在Firefox中支持 - 您现在可以忽略代理)。 - Jason Orendorff
3
好的解决方案。只需添加一个内容,您可以避免使用Temp函数,并使用ES5的Object.create重写前三行代码:var inst = Object.create(Constructor.prototype); - Xose Lluis
1
在Chrome中,这似乎对于“XMLHttpRequest”失败了(我正在使用OS X 10.9.4上的版本37.0.2062.94),导致“TypeError: Failed to construct 'XMLHttpRequest':Please use the 'new' operator, this DOM object constructor cannot be called as a function.”。看起来这是“XMLHttpRequest”的一个特定情况(很可能是一些其他我不知道的对象)。 演示:http://jsfiddle.net/yepygdw9/ - backus
1
太棒了。我不认为有人已经找到了扩展它以允许更多语义调试的方法?我尝试过 Temp.name = Constructor.name 但是这是非法的(name 是只读的)。目前调试非常困难,因为一切都是 Temp,我必须查询实例的 __proto__ 才能找出它们实际上是什么。 - Barney
显示剩余4条评论

16

这个函数在所有情况下都与new相同。 但是,它可能会比999的答案慢很多,因此只有在确实需要时才使用它。

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

更新:一旦ES6支持普及,您将能够编写以下代码:

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

...但你不需要这样做,因为标准库函数Reflect.construct()恰好可以实现你所需的功能!


18
-1 for use of eval - event_jr
这对于复杂参数也行不通,因为参数会被转换为字符串: var circle = new Circle(new Point(10, 10), 10);// [object Point x=10 y=10], 10 - Dave Stewart
2
它运行良好。参数未转换为字符串。试试看。 - Jason Orendorff
6
谢谢,这个方法对我来说最好用了。如果你知道如何使用,eval其实并不那么糟糕,它可以非常有用。 - Steffen Brem

6
在ECMAScript 6中,您可以使用扩展操作符将具有new关键字的构造函数应用于参数数组:
var dateFields = [2014, 09, 20, 19, 31, 59, 999];
var date = new Date(...dateFields);
console.log(date);  // Date 2014-10-20T15:01:59.999Z

不错! - Mark K Cowan

4
另一种方法是修改实际调用的构造函数,但我觉得这比使用eval()或在构造链中引入新的虚拟函数更加清晰... 保留您的conthunktor函数。
function conthunktor(Constructor) {
  // Call the constructor
  return Constructor.apply(null, Array.prototype.slice.call(arguments, 1));
}

并修改被调用的构造函数...

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...
}

那么你可以尝试:

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

var sum = myInstance.a + myInstance.b + myInstance.c; // sum is 6

这对我来说是最好的,干净利落而优雅的解决方案。 - Delta
this instanceof Constructor 检查非常酷,但它防止了构造函数组合(即可扩展的构造函数):function Foo(){}; function Bar(){ Foo.call(this); } - Barney
如果 Bar.prototype = Foo,那么 instanceof 检查应该可以工作。 - 1j01

3

如果Object.create不可用,使用临时构造函数似乎是最好的解决方案。

如果可以使用Object.create,那么使用它是更好的选择。在Node.js上,使用Object.create会得到更快的代码执行速度。以下是Object.create的示例使用方法:

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;
}

(显然,args参数是要应用的参数列表。)
我曾经有一段代码,最初使用eval来读取另一个工具创建的数据。 (是的,eval很邪恶。)这将实例化数百到数千个元素的树。 基本上,JavaScript引擎负责解析和执行一堆新...(...)表达式。 我将系统转换为解析JSON结构,这意味着我的代码必须确定每种对象类型的构造函数调用。 当我在测试套件中运行新代码时,惊讶地看到与eval版本相比出现了明显的减速。
  1. 使用eval版本的测试套件:1秒。
  2. 使用临时构造函数的JSON版本测试套件:5秒。
  3. 使用Object.create的JSON版本测试套件:1秒。
测试套件创建多个树。 当运行测试套件时,我计算出我的applytoConstructor函数被调用约125,000次。

1
这种情况有可重复使用的解决方案。对于您希望使用apply或call方法调用的每个类,您必须先调用convertToAllowApply('classNameInString'),该类必须在相同的作用域或全局作用域中(例如,我没有尝试发送ns.className...)。
以下是代码:
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);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接