Использование .apply () с оператором 'new'. Это возможно? - PullRequest
443 голосов
/ 22 октября 2009

В 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. Тем не менее, люди предложили ряд действительно интересных решений этой проблемы.

Мое предпочтительное решение было это от Мэтью Крамли (я изменил его, чтобы передать свойство arguments):

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

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

Ответы [ 35 ]

349 голосов
/ 13 января 2012

С 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 передается как arugment 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, и мы получим окончательный результат, как указано выше.

259 голосов
/ 22 октября 2009

Вот обобщенное решение, которое может вызывать любой конструктор (кроме собственных конструкторов, которые ведут себя по-разному при вызове функций, таких как String, Number, Date и т. Д.) С массивом аргументов:

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. Иногда его называют модульным шаблоном.

[UPDATE]

Для тех, кто хочет использовать это в TypeScript, поскольку TS выдает ошибку, если F возвращает что-либо:

function construct(constructor, args) {
    function F() : void {
        constructor.apply(this, args);
    }
    F.prototype = constructor.prototype;
    return new F();
}
29 голосов
/ 13 сентября 2015

Если ваша среда поддерживает оператор распространения ECMA Script 2015 (...) , вы можете просто использовать его следующим образом

function Something() {
    // init stuff
}

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

Примечание: Теперь, когда спецификации ECMA Script 2015 опубликованы и большинство движков JavaScript активно его реализуют, это будет предпочтительным способом сделать это.

Вы можете проверить поддержку оператора Spread в нескольких основных средах, здесь .

27 голосов
/ 19 мая 2011

Предположим, у вас есть конструктор Предметов, который отбрасывает все аргументы, которые вы ему набрасываете:

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
14 голосов
/ 21 июля 2016

В ES6 Reflect.construct() довольно удобно:

Reflect.construct(F, args)
9 голосов
/ 18 декабря 2012

@ Мэтью Я думаю, что лучше также исправить свойство конструктора.

// 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;
}
8 голосов
/ 07 декабря 2010

Улучшенная версия ответа @ Мэтью. Эта форма обладает небольшим выигрышем в производительности, полученным благодаря хранению временного класса в замыкании, а также гибкостью использования одной функции, которую можно использовать для создания любого класса

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

7 голосов
/ 22 октября 2009

Вы можете переместить инициализацию в отдельный метод прототипа 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
6 голосов
/ 12 сентября 2011

Этот ответ немного запоздал, но подумал, что любой, кто его увидит, сможет его использовать. Есть способ вернуть новый объект, используя 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;

Чтобы первый оператор if работал в testNew, необходимо ввести return this; в нижней части функции. Итак, в качестве примера с вашим кодом:

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

Обновление: Я изменил свой первый пример, чтобы суммировать любое количество аргументов вместо двух.

6 голосов
/ 26 июля 2014

Я только что столкнулся с этой проблемой и решил ее так:

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

Да, это немного некрасиво, но это решает проблему, и это очень просто.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...