Использование .apply() с "новым" оператором. Это возможно?

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

Ответ 1

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

Ответ 2

Здесь обобщенное решение, которое может вызывать любой конструктор (кроме встроенных конструкторов, которые ведут себя по-разному при вызове функций, таких как 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 от загрязнения глобального пространства имен. Иногда это называется шаблоном модуля.

[ОБНОВЛЕНИЕ]

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

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

Ответ 3

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

function Something() {
    // init stuff
}

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

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

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

Ответ 4

Предположим, что у вас есть конструктор 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

Ответ 5

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

Reflect.construct(F, args)

Ответ 6

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

Ответ 7

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

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

Ответ 8

Вы можете перенести материал init в отдельный метод прототипа 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

Ответ 9

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

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

Ответ 10

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

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

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

Ответ 11

если вас интересует решение на основе eval

function createSomething() {
    var q = [];
    for(var i = 0; i < arguments.length; i++)
        q.push("arguments[" + i + "]");
    return eval("new Something(" + q.join(",") + ")");
}

Ответ 12

Смотрите также, как это делает CoffeeScript.

s = new Something([a,b,c]...)

становится:

var s;
s = (function(func, args, ctor) {
  ctor.prototype = func.prototype;
  var child = new ctor, result = func.apply(child, args);
  return Object(result) === result ? result : child;
})(Something, [a, b, c], function(){});

Ответ 13

Это работает!

var cls = Array; //eval('Array'); dynamically
var data = [2];
new cls(...data);

Ответ 14

Этот подход конструктора работает как с ключевым словом new, так и без него:

function Something(foo, bar){
  if (!(this instanceof Something)){
    var obj = Object.create(Something.prototype);
    return Something.apply(obj, arguments);
  }
  this.foo = foo;
  this.bar = bar;
  return this;
}

Предполагается поддержка Object.create, но вы всегда можете polyfill, если вы поддерживаете старые браузеры. См. таблицу поддержки в MDN здесь.

Здесь JSBin, чтобы увидеть его в действии с выходом консоли.

Ответ 15

Вы не можете вызвать конструктор с переменным числом аргументов, как вы хотите, с помощью оператора new.

Что вы можете сделать, так это немного изменить конструктор. Вместо:

function Something() {
    // deal with the "arguments" array
}
var obj = new Something.apply(null, [0, 0]);  // doesn't work!

Сделайте это вместо:

function Something(args) {
    // shorter, but will substitute a default if args.x is 0, false, "" etc.
    this.x = args.x || SOME_DEFAULT_VALUE;

    // longer, but will only put in a default if args.x is not supplied
    this.x = (args.x !== undefined) ? args.x : SOME_DEFAULT_VALUE;
}
var obj = new Something({x: 0, y: 0});

Или, если вы должны использовать массив:

function Something(args) {
    var x = args[0];
    var y = args[1];
}
var obj = new Something([0, 0]);

Ответ 16

Решения Мэтью Крамли в CoffeeScript:

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
)()

Ответ 17

function createSomething() {
    var args = Array.prototype.concat.apply([null], arguments);
    return new (Function.prototype.bind.apply(Something, args));
}

Если ваш целевой браузер не поддерживает ECMAScript 5 Function.prototype.bind, код не будет работать. Это не очень вероятно, см. таблица совместимости.

Ответ 18

измененный ответ @Matthew. Здесь я могу передать любое количество параметров для работы как обычно (а не массива). Кроме того, "Кое-что" не жестко закодировано:

function createObject( constr ) {   
  var args =  arguments;
  var wrapper =  function() {  
    return constr.apply( this, Array.prototype.slice.call(args, 1) );
  }

  wrapper.prototype =  constr.prototype;
  return  new wrapper();
}


function Something() {
    // init stuff
};

var obj1 =     createObject( Something, 1, 2, 3 );
var same =     new Something( 1, 2, 3 );

Ответ 19

Этот однострочный слой должен сделать это:

new (Function.prototype.bind.apply(Something, [null].concat(arguments)));

Ответ 20

Решение без ES6 или polyfills:

var obj = _new(Demo).apply(["X", "Y", "Z"]);


function _new(constr)
{
    function createNamedFunction(name)
    {
        return (new Function("return function " + name + "() { };"))();
    }

    var func = createNamedFunction(constr.name);
    func.prototype = constr.prototype;
    var self = new func();

    return { apply: function(args) {
        constr.apply(self, args);
        return self;
    } };
}

function Demo()
{
    for(var index in arguments)
    {
        this['arg' + (parseInt(index) + 1)] = arguments[index];
    }
}
Demo.prototype.tagged = true;


console.log(obj);
console.log(obj.tagged);


Выход

Демо {arg1: "X", arg2: "Y", arg3: "Z" }


... или "короче":

var func = new Function("return function " + Demo.name + "() { };")();
func.prototype = Demo.prototype;
var obj = new func();

Demo.apply(obj, ["X", "Y", "Z"]);


изменить
Я думаю, что это может быть хорошим решением:

this.forConstructor = function(constr)
{
    return { apply: function(args)
    {
        let name = constr.name.replace('-', '_');

        let func = (new Function('args', name + '_', " return function " + name + "() { " + name + "_.apply(this, args); }"))(args, constr);
        func.constructor = constr;
        func.prototype = constr.prototype;

        return new func(args);
    }};
}

Ответ 22

Любая функция (даже конструктор) может принимать переменное количество аргументов. Каждая функция имеет переменную "аргументы", которая может быть передана в массив с помощью [].slice.call(arguments).

function Something(){
  this.options  = [].slice.call(arguments);

  this.toString = function (){
    return this.options.toString();
  };
}

var s = new Something(1, 2, 3, 4);
console.log( 's.options === "1,2,3,4":', (s.options == '1,2,3,4') );

var z = new Something(9, 10, 11);
console.log( 'z.options === "9,10,11":', (z.options == '9,10,11') );

Вышеуказанные тесты производят следующий вывод:

s.options === "1,2,3,4": true
z.options === "9,10,11": true

Ответ 23

function FooFactory() {
    var prototype, F = function(){};

    function Foo() {
        var args = Array.prototype.slice.call(arguments),
            i;     
        for (i = 0, this.args = {}; i < args.length; i +=1) {
            this.args[i] = args[i];
        }
        this.bar = 'baz';
        this.print();

        return this;
    }

    prototype = Foo.prototype;
    prototype.print = function () {
        console.log(this.bar);
    };

    F.prototype = prototype;

    return Foo.apply(new F(), Array.prototype.slice.call(arguments));
}

var foo = FooFactory('a', 'b', 'c', 'd', {}, function (){});
console.log('foo:',foo);
foo.print();

Ответ 24

Вот моя версия createSomething:

function createSomething() {
    var obj = {};
    obj = Something.apply(obj, arguments) || obj;
    obj.__proto__ = Something.prototype; //Object.setPrototypeOf(obj, Something.prototype); 
    return o;
}

Исходя из этого, я попытался имитировать ключевое слово new JavaScript:

//JavaScript 'new' keyword simulation
function new2() {
    var obj = {}, args = Array.prototype.slice.call(arguments), fn = args.shift();
    obj = fn.apply(obj, args) || obj;
    Object.setPrototypeOf(obj, fn.prototype); //or: obj.__proto__ = fn.prototype;
    return obj;
}

Я тестировал его, и кажется, что он работает отлично для всех сценариев. Он также работает с собственными конструкторами типа Date. Вот несколько тестов:

//test
new2(Something);
new2(Something, 1, 2);

new2(Date);         //"Tue May 13 2014 01:01:09 GMT-0700" == new Date()
new2(Array);        //[]                                  == new Array()
new2(Array, 3);     //[undefined × 3]                     == new Array(3)
new2(Object);       //Object {}                           == new Object()
new2(Object, 2);    //Number {}                           == new Object(2)
new2(Object, "s");  //String {0: "s", length: 1}          == new Object("s")
new2(Object, true); //Boolean {}                          == new Object(true)

Ответ 25

Да, мы можем, javascript больше похож на prototype inheritance в природе.

function Actor(name, age){
  this.name = name;
  this.age = age;
}

Actor.prototype.name = "unknown";
Actor.prototype.age = "unknown";

Actor.prototype.getName = function() {
    return this.name;
};

Actor.prototype.getAge = function() {
    return this.age;
};

когда мы создаем объект с "new", то наш созданный объект INHERITS getAge(), но если мы использовали apply(...) or call(...) для вызова Actor, то мы передаем объект для "this", но объект мы передать WON'T наследовать от Actor.prototype

если мы непосредственно не передадим заявку или не позволим Actor.prototype, а затем.... "this" будет указывать на "Actor.prototype", и this.name будет писать: Actor.prototype.name. Таким образом, затрагивая все другие объекты, созданные с помощью Actor..., поскольку мы перезаписываем прототип, а не экземпляр

var rajini = new Actor('Rajinikanth', 31);
console.log(rajini);
console.log(rajini.getName());
console.log(rajini.getAge());

var kamal = new Actor('kamal', 18);
console.log(kamal);
console.log(kamal.getName());
console.log(kamal.getAge());

Попробуйте с помощью apply

var vijay = Actor.apply(null, ["pandaram", 33]);
if (vijay === undefined) {
    console.log("Actor(....) didn't return anything 
           since we didn't call it with new");
}

var ajith = {};
Actor.apply(ajith, ['ajith', 25]);
console.log(ajith); //Object {name: "ajith", age: 25}
try {
    ajith.getName();
} catch (E) {
    console.log("Error since we didn't inherit ajith.prototype");
}
console.log(Actor.prototype.age); //Unknown
console.log(Actor.prototype.name); //Unknown

Передав Actor.prototype в Actor.call() в качестве первого аргумента, когда функция Actor() запущена, она выполняет this.name=name, так как "this" будет указывать на Actor.prototype, this.name=name; means Actor.prototype.name=name;

var simbhu = Actor.apply(Actor.prototype, ['simbhu', 28]);
if (simbhu === undefined) {
    console.log("Still undefined since the function didn't return anything.");
}
console.log(Actor.prototype.age); //simbhu
console.log(Actor.prototype.name); //28

var copy = Actor.prototype;
var dhanush = Actor.apply(copy, ["dhanush", 11]);
console.log(dhanush);
console.log("But now we've corrupted Parent.prototype in order to inherit");
console.log(Actor.prototype.age); //11
console.log(Actor.prototype.name); //dhanush

Возвращаясь к оригинальному вопросу о том, как использовать new operator with apply, вот мой прием....

Function.prototype.new = function(){
    var constructor = this;
    function fn() {return constructor.apply(this, args)}
    var args = Array.prototype.slice.call(arguments);
    fn.prototype = this.prototype;
    return new fn
};

var thalaivar = Actor.new.apply(Parent, ["Thalaivar", 30]);
console.log(thalaivar);

Ответ 26

В то время как другие подходы работоспособны, они чрезмерно сложны. В Clojure вы обычно создаете функцию, которая создает экземпляры/записи и использует эту функцию в качестве механизма для создания экземпляра. Переведя это на JavaScript:

function Person(surname, name){
  this.surname = surname;
  this.name = name;
}

function person(surname, name){ 
  return new Person(surname, name);
}

Используя этот подход, вы избегаете использования new, за исключением случаев, описанных выше. И эта функция, конечно, не имеет проблем с apply или любым количеством других функций функционального программирования.

var doe  = _.partial(person, "Doe");
var john = doe("John");
var jane = doe("Jane");

Используя этот подход, все конструкторы вашего типа (например, Person) являются ванильными конструкторами do-nothing. Вы просто передаете аргументы и присваиваете их свойствам с тем же именем. Волосатые детали входят в функцию конструктора (например, Person).

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

Ответ 28

На самом деле самый простой способ:

function Something (a, b) {
  this.a = a;
  this.b = b;
}
function createSomething(){
    return Something;
}
s = new (createSomething())(1, 2); 
// s == Something {a: 1, b: 2}

Ответ 29

Пересмотренное решение из ответа @jordancpaul.

var applyCtor = function(ctor, args)
{
    var instance = new ctor();
    ctor.prototype.constructor.apply(instance, args);
    return instance;
}; 

Ответ 30

Благодаря сообщениям здесь я использовал его следующим образом:

SomeClass = function(arg1, arg2) {
    // ...
}

ReflectUtil.newInstance('SomeClass', 5, 7);

и реализация:

/**
 * @param strClass:
 *          class name
 * @param optionals:
 *          constructor arguments
 */
ReflectUtil.newInstance = function(strClass) {
    var args = Array.prototype.slice.call(arguments, 1);
    var clsClass = eval(strClass);
    function F() {
        return clsClass.apply(this, args);
    }
    F.prototype = clsClass.prototype;
    return new F();
};