使用.apply()方法和'new'操作符,这种方式可行吗?

483
在JavaScript中,我想通过new运算符创建一个对象实例,但是要向构造函数传递任意数量的参数。这是否可能?
我想做的事情类似于下面的代码(但是下面的代码不起作用):
function Something(){
    // init stuff
}
function createSomething(){
    return new Something.apply(null, arguments);
}
var s = createSomething(a,b,c); // 's' is an instance of Something

答案

从这里的回答中,可以清楚地看到没有内置的方法可以使用 .apply()new 操作符一起调用。然而,有人提供了一些非常有趣的解决方案。

我倾向于使用Matthew Crumley的这个解决方案(我已经修改过以传递arguments属性):

var createSomething = (function() {
    function F(args) {
        return Something.apply(this, args);
    }
    F.prototype = Something.prototype;

    return function() {
        return new F(arguments);
    }
})();

2
在CoffeeScript中,Matthew Crumley的解决方案如下:construct = (constructor, args) -> F = -> constructor.apply this,args F.prototype = constructor.prototype new F createSomething = (()-> F = (args) -> Something.apply this.args F.prototype = Something.prototype return -> new Something arguments )() - Benjie
4
我认为这篇文章的重点是new有两个作用:(i) 设置原型, (ii) 将构造器应用于具有“this”设置为该对象/原型组合的对象。您可以通过使用 Object.create() 或通过自己扩展Object.create()并使用闭包捕获上下文来实现这一点。 - Dmitry Minkovsky
我通过将类作为参数传递到外部函数中来进行了泛化。因此,这基本上是一个工厂-工厂。 - Killroy
@Pumbaa80的答案似乎是更好的解决方案,也被ES6 Traceur用于填充spread运算符。 =)在Chrome中它也稍微快一点:http://jsperf.com/dynamic-arguments-to-the-constructor - Sergey Kamardin
有人能解释一下为什么这个人不能像这样做 var s = new Something(a,b,c) 吗?我不明白 :/ - through.a.haze
MDN的最新更新:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Spread_operator#Apply_for_new - Alwin Kesler
36个回答

387

使用ECMAScript5的Function.prototype.bind可以使事情变得更加简洁:

function newCall(Cls) {
    return new (Function.prototype.bind.apply(Cls, arguments));
    // or even
    // return new (Cls.bind.apply(Cls, arguments));
    // if you know that Cls.bind has not been overwritten
}

它可以按如下方式使用:

var s = newCall(Something, a, b, c);

或者直接这样:

var s = new (Function.prototype.bind.call(Something, null, a, b, c));

var s = new (Function.prototype.bind.apply(Something, [null, a, b, c]));

这个和基于eval的解决方案是唯二的总是可用的解决方案,即使使用像Date这样的特殊构造函数:

var date = newCall(Date, 2012, 1);
console.log(date instanceof Date); // true

编辑

简单解释一下: 我们需要在一个只接受有限数量参数的函数上运行new。使用bind方法可以这样做:

var f = Cls.bind(anything, arg1, arg2, ...);
result = new f();

anything参数并不重要,因为new关键字会重置f的上下文。但是,出于语法原因,它是必需的。现在,对于bind调用:我们需要传递可变数量的参数,因此这样做就可以了:

var f = Cls.bind.apply(Cls, [anything, arg1, arg2, ...]);
result = new f();

让我们把它封装在一个函数中。Cls作为第0个参数传递,因此它将是我们的anything

function newCall(Cls /*, arg1, arg2, ... */) {
    var f = Cls.bind.apply(Cls, arguments);
    return new f();
}

实际上,临时变量 f 根本不需要:

function newCall(Cls /*, arg1, arg2, ... */) {
    return new (Cls.bind.apply(Cls, arguments))();
}

最终,我们应该确保bind是我们实际需要的。(Cls.bind可能已被覆盖)。所以用Function.prototype.bind替换它,我们就能得到上面的最终结果。


3
你说的“Function.bind可以代替Function.prototype.bind”是正确的,我只是为了清晰明了而留下了它。毕竟,任何函数都可以使用:eval.bind可以节省更多代码,但那样会更加令人困惑。 - user123444555621
2
有趣的是,但重要的是要记住IE8仅支持ECMAScript 3 - jordancpaul
3
我发现你的解决方案很好,但有些令人困惑。因为在第一个示例中,你写了newCall(Something,a,b,c),其中a将作为绑定的上下文,但在你的第二个示例中,你提到上下文是无意义的 - 所以你发送null。对我来说,这非常令人困惑(你相信我曾经考虑了3天吗?)- 无论如何,你的第一个代码(为了与第二个示例保持一致)需要是:function newCall(Cls) {var arr=Array.prototype.slice.call(arguments);arr.shift(); return new (Function.prototype.bind.apply(Cls, [null,arr])); - Royi Namir
4
更正:将[null,arr]改为[null].concat(arr) - Royi Namir
1
你成功地做到了大多数人失败的事情:对象是正确类型的,我以后可以使用instanceof等。谢谢! - Wilt
显示剩余20条评论

260

这是一个通用解决方案,可以使用参数数组调用任何构造函数(除了像StringNumberDate等被当作函数调用时行为不同的本地构造函数):

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

通过调用construct(Class,[1, 2, 3])创建的对象与使用new Class(1, 2, 3)创建的对象是相同的。

您还可以制作更具体的版本,以便每次不必传递构造函数。这也稍微更有效率,因为它不需要每次调用时创建内部函数的新实例。

var createSomething = (function() {
    function F(args) {
        return Something.apply(this, args);
    }
    F.prototype = Something.prototype;

    return function(args) {
        return new F(args);
    }
})();

创建和调用外部匿名函数的原因是使函数 F 不会污染全局命名空间。这有时被称为模块模式。

[更新]

对于那些想在 TypeScript 中使用它的人,由于 TS 如果 F 返回任何内容都会报错:

function construct(constructor, args) {
    function F() : void {
        constructor.apply(this, args);
    }
    F.prototype = constructor.prototype;
    return new F();
}

2
谢谢Matthew。动态调用闭包很有趣。虽然你的例子展示了调用函数只允许一个参数(一个参数数组),但我猜这可以修改为传递arguments变量。 - Prem
2
这不适用于DateString或任何其他在作为构造函数调用时行为不同的函数。 - user123444555621
2
嗨,Matthew,最好也修复构造函数属性。你的答案的增强版本。https://dev59.com/5HI-5IYBdhLWcg3w8NOB#13931627 - wukong
这种方法无法与一些具有自己奇怪继承代码的库一起使用。例如,KineticJS - AnnanFay
3
其中一个缺点是,构造出的对象的 myObj.constructor.name 属性将被设置为 F。在调试器中查看堆栈跟踪和详细转储时,这有点糟糕,因为它们在各个地方使用该名称。 - goat
显示剩余5条评论

37
如果您的环境支持 ECMA Script 2015 的扩展运算符(..., 您可以像这样简单地使用它。
function Something() {
    // init stuff
}

function createSomething() {
    return new Something(...arguments);
}

注意:现在ECMA Script 2015规范已经发布,大多数JavaScript引擎正在积极实现它,这将成为执行此操作的首选方法。
您可以在一些主要环境中检查Spread运算符的支持情况,在这里

1
这是一个非常好的解决方案。它还适用于这种情况,而@user123444555621的解决方案失败了。 - Wilt
3
@Wilt: 在 bind.apply 中,JSFiddle 必须使用 [null].concat(args) 而不是仅仅使用 args。请查看修复版 - trss
@trss 那个数组中的额外 null 是做什么用的? - Simon D
这是在 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/apply 上提到的 thisArg - trss

27

假设你有一个Items构造函数,它会接收你传递给它的所有参数:

function Items () {
    this.elems = [].slice.call(arguments);
}

Items.prototype.sum = function () {
    return this.elems.reduce(function (sum, x) { return sum + x }, 0);
};

您可以使用Object.create()创建一个实例,然后应用.apply()该实例:

var items = Object.create(Items.prototype);
Items.apply(items, [ 1, 2, 3, 4 ]);

console.log(items.sum());

当运行时,会打印出 10,因为 1 + 2 + 3 + 4 == 10:

$ node t.js
10

2
如果您有Object.create可用,则这是另一种好的实现方式。 - Matthew Crumley
所有其他答案在过去都很好,但是现在这种方法是明显的赢家。将来,Reflect.construct可能会更好。 - Jelle De Loecker

25

我一直在尝试理解如何使用Reflect.construct来将“this”应用于我正在构造的新类“F”,但似乎F内部的“this”的上下文仍然是新的,旧的“this”属性不会传递过来,有没有人遇到过任何好的博客文章或示例说明如何使用“Reflect.construct()”? - trickpatty
1
最佳答案在这里! - Thomas Wagenaar
这对我很有效 - 非常好! - lmiller1990

9

@Matthew 我认为最好也修复构造函数属性。

// Invoke new operator with arbitrary arguments
// Holy Grail pattern
function invoke(constructor, args) {
    var f;
    function F() {
        // constructor returns **this**
        return constructor.apply(this, args);
    }
    F.prototype = constructor.prototype;
    f = new F();
    f.constructor = constructor;
    return f;
}

我认为你是正确的,修复构造函数是有用的,除非应用程序从未检查它,在编写库时无法暗示。 - Jean Vincent
1
我已经在调试器中查看了它(在断点处评估newObject.constructor == ConstructorFunction)。结果发现根本不需要赋值,因为在@MatthewCrumley的代码中已经正确设置了。 - hashchange

8
你可以将初始化的内容移动到Something原型的一个单独方法中:
function Something() {
    // Do nothing
}

Something.prototype.init = function() {
    // Do init stuff
};

function createSomething() {
    var s = new Something();
    s.init.apply(s, arguments);
    return s;
}

var s = createSomething(a,b,c); // 's' is an instance of Something

是的,好主意。我可以创建一个init()方法,然后在其上使用apply()。就像我对Ionut方法的评论一样,有点遗憾的是没有一种方法可以在不修改构造函数体系结构的情况下完成这个任务。但是这看起来是一个实用的解决方案。 - Prem

8
@Matthew的回答的改进版本。这个形式通过将temp类存储在闭包中获得了轻微的性能优势,同时具有一个函数能够用于创建任何类的灵活性。
var applyCtor = function(){
    var tempCtor = function() {};
    return function(ctor, args){
        tempCtor.prototype = ctor.prototype;
        var instance = new tempCtor();
        ctor.prototype.constructor.apply(instance,args);
        return instance;
    }
}();

调用applyCtor(class, [arg1, arg2, argn]);来使用此功能。


6

虽然有点晚,但我认为任何看到这篇文章的人都可以使用它。使用apply方法可以返回一个新的对象,但需要对你的对象声明做出一点改变。

function testNew() {
    if (!( this instanceof arguments.callee ))
        return arguments.callee.apply( new arguments.callee(), arguments );
    this.arg = Array.prototype.slice.call( arguments );
    return this;
}

testNew.prototype.addThem = function() {
    var newVal = 0,
        i = 0;
    for ( ; i < this.arg.length; i++ ) {
        newVal += this.arg[i];
    }
    return newVal;
}

testNew( 4, 8 ) === { arg : [ 4, 8 ] };
testNew( 1, 2, 3, 4, 5 ).addThem() === 15;

要使testNew函数中的第一个if语句生效,您需要在函数底部添加return this;。因此,以下是使用您的代码示例:

function Something() {
    // init stuff
    return this;
}
function createSomething() {
    return Something.apply( new Something(), arguments );
}
var s = createSomething( a, b, c );

更新:我已经修改了我的第一个例子,现在可以对任意数量的参数求和,而不仅仅是两个。


1
当然,arguments.callee已经被弃用(https://developer.mozilla.org/en/JavaScript/Reference/Functions_and_function_scope/arguments/callee),但您仍然可以直接通过名称引用您的构造函数。 - busticated
@busticated:完全正确,而且我在第二个片段中没有使用它。 - Trevor Norris
1
这个解决方案唯一的问题是它最终会在同一个对象上使用不同的参数运行构造函数两次。 - Will Tomlins
1
@WillTomlins:它最终将会运行构造函数两次,并且在不同的上下文中。虽然我不确定参数会如何改变。能否澄清一下? - Trevor Norris
1
我只是指对象将被创建,然后构造函数将在该对象上运行,而不带任何参数,然后构造函数将再次在同一对象上运行,这次使用a、b和c。当您的构造函数需要某些参数时,您可能会发现一些尴尬的情况出现。 - Will Tomlins

6

我刚刚遇到了这个问题,我是这样解决的:

function instantiate(ctor) {
    switch (arguments.length) {
        case 1: return new ctor();
        case 2: return new ctor(arguments[1]);
        case 3: return new ctor(arguments[1], arguments[2]);
        case 4: return new ctor(arguments[1], arguments[2], arguments[3]);
        //...
        default: throw new Error('instantiate: too many parameters');
    }
}

function Thing(a, b, c) {
    console.log(a);
    console.log(b);
    console.log(c);
}

var thing = instantiate(Thing, 'abc', 123, {x:5});

是的,这有点丑陋,但它解决了问题,而且非常简单。


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