Использование .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);
}};
}
Ответ 21
Также интересно посмотреть, как проблема повторного использования временного конструктора F()
, была решена с помощью arguments.callee
, а также самой функции creator/factory:
http://www.dhtmlkitchen.com/?category=/JavaScript/&date=2008/05/11/&entry=Decorator-Factory-Aspect
Ответ 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
).
Не нужно беспокоиться о том, чтобы создавать эти дополнительные функции конструктора, так как они являются хорошей практикой. Они могут быть удобными, поскольку они позволяют потенциально иметь несколько функций-конструкторов с различными нюансами.
Ответ 27
так как ES6 это возможно через оператор Spread, см. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_operator#Apply_for_new
Этот ответ уже был приведен в комментарии fooobar.com/questions/16554/..., но, похоже, был пропущен большинством
Ответ 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();
};