javascript - method - Uso de.apply() con el operador ''nuevo''. es posible?
jquery apply (30)
¡Esto funciona!
var cls = Array; //eval(''Array''); dynamically
var data = [2];
new cls(...data);
En JavaScript, quiero crear una instancia de objeto (a través del new
operador), pero pasar un número arbitrario de argumentos al constructor. es posible?
Lo que quiero hacer es algo como esto (pero el siguiente código no funciona):
function Something(){
// init stuff
}
function createSomething(){
return new Something.apply(null, arguments);
}
var s = createSomething(a,b,c); // ''s'' is an instance of Something
La respuesta
De las respuestas aquí, quedó claro que no hay una forma .apply()
llamar a .apply()
con el new
operador. Sin embargo, la gente sugirió una serie de soluciones realmente interesantes para el problema.
Mi solución preferida fue esta de Matthew Crumley (la he modificado para pasar la propiedad de los arguments
):
var createSomething = (function() {
function F(args) {
return Something.apply(this, args);
}
F.prototype = Something.prototype;
return function() {
return new F(arguments);
}
})();
@Matthew Creo que es mejor arreglar la propiedad del constructor también.
// 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;
}
Acabo de encontrar este problema y lo resolví así:
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});
Sí, es un poco feo, pero resuelve el problema, y es muy simple.
Aquí está mi versión de createSomething
:
function createSomething() {
var obj = {};
obj = Something.apply(obj, arguments) || obj;
obj.__proto__ = Something.prototype; //Object.setPrototypeOf(obj, Something.prototype);
return o;
}
En base a eso, traté de simular la new
palabra clave de 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;
}
Lo probé y parece que funciona perfectamente bien para todos los escenarios. También funciona en constructores nativos como Date
. Aquí hay algunas pruebas:
//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)
Aquí hay una solución generalizada que puede llamar a cualquier constructor (excepto constructores nativos que se comportan de manera diferente cuando se les llama como funciones, como String
, Number
, Date
, etc.) con una matriz de argumentos:
function construct(constructor, args) {
function F() {
return constructor.apply(this, args);
}
F.prototype = constructor.prototype;
return new F();
}
Un objeto creado al llamar a la construct(Class, [1, 2, 3])
sería idéntico a un objeto creado con la new Class(1, 2, 3)
.
También puedes hacer una versión más específica para no tener que pasar el constructor cada vez. Esto también es un poco más eficiente, ya que no necesita crear una nueva instancia de la función interna cada vez que la llame.
var createSomething = (function() {
function F(args) {
return Something.apply(this, args);
}
F.prototype = Something.prototype;
return function(args) {
return new F(args);
}
})();
La razón para crear y llamar a la función anónima externa de esta manera es evitar que la función F
contamine el espacio de nombres global. A veces se le llama el patrón del módulo.
[ACTUALIZAR]
Para aquellos que quieren usar esto en TypeScript, ya que TS da un error si F
devuelve algo:
function construct(constructor, args) {
function F() : void {
constructor.apply(this, args);
}
F.prototype = constructor.prototype;
return new F();
}
Con Function.prototype.bind
de ECMAScript5, las cosas se ponen bastante limpias:
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
}
Se puede utilizar de la siguiente manera:
var s = newCall(Something, a, b, c);
o incluso directamente:
var s = new (Function.prototype.bind.call(Something, null, a, b, c));
var s = new (Function.prototype.bind.apply(Something, [null, a, b, c]));
Esta y la solución basada en eval son las únicas que siempre funcionan, incluso con constructores especiales como Date
:
var date = newCall(Date, 2012, 1);
console.log(date instanceof Date); // true
editar
Un poco de explicación: necesitamos ejecutar una new
función en una función que tenga un número limitado de argumentos. El método de bind
nos permite hacerlo así:
var f = Cls.bind(anything, arg1, arg2, ...);
result = new f();
El parámetro de anything
no importa mucho, ya que la new
palabra clave restablece el contexto de f
. Sin embargo, se requiere por razones sintácticas. Ahora, para la llamada de bind
: Necesitamos pasar un número variable de argumentos, así que esto hace el truco:
var f = Cls.bind.apply(Cls, [anything, arg1, arg2, ...]);
result = new f();
Vamos a envolver eso en una función. Cls
se pasa como argumento 0, por lo que será nuestra anything
.
function newCall(Cls /*, arg1, arg2, ... */) {
var f = Cls.bind.apply(Cls, arguments);
return new f();
}
En realidad, la variable f
temporal no es necesaria en absoluto:
function newCall(Cls /*, arg1, arg2, ... */) {
return new (Cls.bind.apply(Cls, arguments))();
}
Por último, debemos asegurarnos de que el bind
es realmente lo que necesitamos. ( Cls.bind
puede haber sido sobrescrito). Reemplácelo por Function.prototype.bind
, y obtendremos el resultado final como se indica arriba.
Cualquier función (incluso un constructor) puede tomar un número variable de argumentos. Cada función tiene una variable de "argumentos" que se puede convertir a una matriz con [].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'') );
Las pruebas anteriores producen el siguiente resultado:
s.options === "1,2,3,4": true
z.options === "9,10,11": true
En ES6, Reflect.construct()
es bastante conveniente:
Reflect.construct(F, args)
En realidad el método más simple es:
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}
Esta respuesta es un poco tarde, pero imagino que cualquiera que vea esto podría usarla. Hay una manera de devolver un nuevo objeto utilizando aplicar. Aunque requiere un pequeño cambio en su declaración de objeto.
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;
Para que la primera instrucción if
funcione en la testNew
, tienes que return this;
en la parte inferior de la función. Así como un ejemplo con su código:
function Something() {
// init stuff
return this;
}
function createSomething() {
return Something.apply( new Something(), arguments );
}
var s = createSomething( a, b, c );
Actualización: He cambiado mi primer ejemplo para sumar cualquier número de argumentos, en lugar de solo dos.
Este enfoque de constructor funciona con y sin la new
palabra clave:
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;
}
Se supone que es compatible con Object.create
pero siempre puede realizar un Object.create
si está admitiendo navegadores más antiguos. Vea la tabla de soporte en MDN aquí .
Aquí hay un JSBin para verlo en acción con la salida de la consola .
Este one-liner debe hacerlo:
new (Function.prototype.bind.apply(Something, [null].concat(arguments)));
Gracias a las publicaciones aquí lo he usado de esta manera:
SomeClass = function(arg1, arg2) {
// ...
}
ReflectUtil.newInstance(''SomeClass'', 5, 7);
e implementación:
/**
* @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();
};
Mientras que los otros enfoques son viables, son excesivamente complejos. En Clojure generalmente creas una función que crea instancias de tipos / registros y usas esa función como el mecanismo para crear instancias. Traduciendo esto a JavaScript:
function Person(surname, name){
this.surname = surname;
this.name = name;
}
function person(surname, name){
return new Person(surname, name);
}
Al adoptar este enfoque, evita el uso de new
excepto como se describe anteriormente. Y esta función, por supuesto, no tiene problemas al trabajar con apply
ni ninguna otra función funcional de programación.
var doe = _.partial(person, "Doe");
var john = doe("John");
var jane = doe("Jane");
Al utilizar este enfoque, todos los constructores de tipo (por ejemplo, Person
) son constructores de vainilla, de no hacer nada. Solo pasas los argumentos y los asignas a propiedades del mismo nombre. Los detalles peludos van en la función de constructor (por ejemplo, person
).
No es una molestia tener que crear estas funciones constructoras adicionales, ya que de todos modos son una buena práctica. Pueden ser convenientes, ya que le permiten tener varias funciones de constructor con diferentes matices.
No puede llamar a un constructor con un número variable de argumentos como quiera con el new
operador.
Lo que puedes hacer es cambiar el constructor ligeramente. En lugar de:
function Something() {
// deal with the "arguments" array
}
var obj = new Something.apply(null, [0, 0]); // doesn''t work!
Haga esto en su lugar:
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});
O si debes usar una matriz:
function Something(args) {
var x = args[0];
var y = args[1];
}
var obj = new Something([0, 0]);
Puedes mover el material de inicio a un método separado del prototipo de 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
Respuesta modificada de @Matthew. Aquí puedo pasar cualquier número de parámetros para que funcione como de costumbre (no para una matriz). También ''Algo'' no está codificado en:
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 );
Sí podemos, javascript es más de un prototype inheritance
en la naturaleza.
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;
};
cuando creamos un objeto con " new
", nuestro objeto creado INHERITS getAge
(), pero si usamos apply(...) or call(...)
para llamar a Actor, entonces estamos pasando un objeto para "this"
pero el objeto que pasamos WON''T
heredará de Actor.prototype
a menos que pasemos directamente aplicar o llamar Actor.prototype pero luego ... "esto" apuntaría a "Actor.prototype" y este.nombre escribiría a: Actor.prototype.name
. Esto afecta a todos los demás objetos creados con Actor...
ya que sobrescribimos el prototipo en lugar de la instancia
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());
Probemos con 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
Al pasar Actor.prototype
a Actor.call()
como primer argumento, cuando se ejecuta la función Actor (), ejecuta this.name=name
, ya que "this" apuntará a Actor.prototype
, this.name=name; means Actor.prototype.name=name;
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
Volviendo a la pregunta original sobre cómo usar un new operator with apply
, aquí está mi opinión ...
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);
Si estás interesado en una solución basada en evaluación.
function createSomething() {
var q = [];
for(var i = 0; i < arguments.length; i++)
q.push("arguments[" + i + "]");
return eval("new Something(" + q.join(",") + ")");
}
Si su entorno es compatible con el operador de propagación de ECMA Script 2015 ( ...
) , simplemente puede usarlo así
function Something() {
// init stuff
}
function createSomething() {
return new Something(...arguments);
}
Nota: Ahora que se han publicado las especificaciones de ECMA Script 2015 y que la mayoría de los motores de JavaScript lo están implementando activamente, esta sería la forma preferida de hacerlo.
Puede consultar el soporte del operador Spread en algunos de los entornos principales, here .
Solución sin ES6 o 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);
salida
Demo {arg1: "X", arg2: "Y", arg3: "Z"}
... o forma más corta:
var func = new Function("return function " + Demo.name + "() { };")();
func.prototype = Demo.prototype;
var obj = new func();
Demo.apply(obj, ["X", "Y", "Z"]);
editar:
Creo que esta podría ser una buena solución:
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);
}};
}
Supongamos que tienes un constructor de elementos que absorbe todos los argumentos que le presentas:
function Items () {
this.elems = [].slice.call(arguments);
}
Items.prototype.sum = function () {
return this.elems.reduce(function (sum, x) { return sum + x }, 0);
};
Puede crear una instancia con Object.create () y luego .apply () con esa instancia:
var items = Object.create(Items.prototype);
Items.apply(items, [ 1, 2, 3, 4 ]);
console.log(items.sum());
Que cuando se ejecuta imprime 10 desde 1 + 2 + 3 + 4 == 10:
$ node t.js
10
También es interesante ver cómo se resolvió el problema de reutilizar el constructor temporal F()
mediante arguments.callee
, también conocida como la función creadora / fábrica: http://www.dhtmlkitchen.com/?category=/JavaScript/&date=2008/05/11/&entry=Decorator-Factory-Aspect
Una solución revisada de la respuesta de @jordancpaul.
var applyCtor = function(ctor, args)
{
var instance = new ctor();
ctor.prototype.constructor.apply(instance, args);
return instance;
};
Una versión mejorada de la respuesta de @ Matthew. Esta forma tiene los beneficios leves de rendimiento obtenidos al almacenar la clase temporal en un cierre, así como la flexibilidad de tener una función que se puede usar para crear cualquier clase
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;
}
}();
Esto se usaría llamando a applyCtor(class, [arg1, arg2, argn]);
Vea también cómo CoffeeScript lo hace.
s = new Something([a,b,c]...)
se convierte en:
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(){});
ya que ES6 esto es posible a través del operador Spread, consulte https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_operator#Apply_for_new
Esta respuesta ya estaba, más o menos dada en el comentario https://.com/a/42027742/7049810 , pero parece que la mayoría la ha pasado por alto.
Soluciones de Matthew Crumley en CoffeeScript:
construct = (constructor, args) ->
F = -> constructor.apply this, args
F.prototype = constructor.prototype
new F
o
createSomething = (->
F = (args) -> Something.apply this, args
F.prototype = Something.prototype
return -> new Something arguments
)()
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();
function createSomething() {
var args = Array.prototype.concat.apply([null], arguments);
return new (Function.prototype.bind.apply(Something, args));
}
Si su navegador de destino no es compatible con ECMAScript 5 Function.prototype.bind
, el código no funcionará. Aunque no es muy probable, ver tabla de compatibilidad .