2010-07-29 11 views
78

¿Cómo podría generalizar la siguiente función para tomar N argumentos? (¿Usar llamadas o postularse?)¿Cómo puedo llamar a un constructor de Javascript usando call o apply?

¿Existe alguna manera programática de aplicar argumentos a 'nuevo'? No quiero que el constructor sea tratado como una función simple.

/** 
* This higher level function takes a constructor and arguments 
* and returns a function, which when called will return the 
* lazily constructed value. 
* 
* All the arguments, except the first are pased to the constructor. 
* 
* @param {Function} constructor 
*/ 

function conthunktor(Constructor) { 
    var args = Array.prototype.slice.call(arguments, 1); 
    return function() { 
     console.log(args); 
     if (args.length === 0) { 
      return new Constructor(); 
     } 
     if (args.length === 1) { 
      return new Constructor(args[0]); 
     } 
     if (args.length === 2) { 
      return new Constructor(args[0], args[1]); 
     } 
     if (args.length === 3) { 
      return new Constructor(args[0], args[1], args[2]); 
     } 
     throw("too many arguments");  
    } 
} 

prueba qUnit:

test("conthunktorTest", function() { 
    function MyConstructor(arg0, arg1) { 
     this.arg0 = arg0; 
     this.arg1 = arg1; 
    } 
    MyConstructor.prototype.toString = function() { 
     return this.arg0 + " " + this.arg1; 
    } 

    var thunk = conthunktor(MyConstructor, "hello", "world"); 
    var my_object = thunk(); 
    deepEqual(my_object.toString(), "hello world"); 
}); 
+1

Ben Nadel [escribió sobre esto] (http: //www.bennadel. com/blog/2291-Invocando-A-Native-JavaScript-Constructor-Using-Call-Or-Apply-.htm) ampliamente. –

Respuesta

49

Prueba esto:

function conthunktor(Constructor) { 
    var args = Array.prototype.slice.call(arguments, 1); 
    return function() { 

     var Temp = function(){}, // temporary constructor 
      inst, ret; // other vars 

     // Give the Temp constructor the Constructor's prototype 
     Temp.prototype = Constructor.prototype; 

     // Create a new instance 
     inst = new Temp; 

     // Call the original Constructor with the temp 
     // instance as its context (i.e. its 'this' value) 
     ret = Constructor.apply(inst, args); 

     // If an object has been returned then return it otherwise 
     // return the original instance. 
     // (consistent with behaviour of the new operator) 
     return Object(ret) === ret ? ret : inst; 

    } 
} 
+1

Gracias, eso funciona en el código de prueba. ¿Es su comportamiento idéntico al nuevo? (es decir, no hay problemas desagradables para encontrar). – fadedbee

+0

@chrisdew, he agregado algunos comentarios al código. – James

+0

Gracias eso es mucho más claro. – fadedbee

15

Esta función es idéntica a new en todos los casos. Sin embargo, probablemente sea significativamente más lento que la respuesta de 999, así que úsela solo si realmente la necesita.

function applyConstructor(ctor, args) { 
    var a = []; 
    for (var i = 0; i < args.length; i++) 
     a[i] = 'args[' + i + ']'; 
    return eval('new ctor(' + a.join() + ')'); 
} 

ACTUALIZACIÓN: Una vez apoyo ES6 está muy extendida, podrás escribir esto:

function applyConstructor(ctor, args) { 
    return new ctor(...args); 
} 

... pero usted no tendrá que, debido a que la función de la biblioteca estándar de Reflect.construct() hace exactamente lo que estás buscando!

+14

-1 para el uso de eval –

+0

Eso no funcionará para parámetros complejos, ya que los argumentos se convierten en cadenas: var circle = new Circle (nuevo punto (10, 10), 10); // [objeto Punto x = 10 y = 10], 10 –

+2

Funciona bien. Los argumentos no se convierten en cadenas. Intentalo. –

89

Esta es la forma de hacerlo:

function applyToConstructor(constructor, argArray) { 
    var args = [null].concat(argArray); 
    var factoryFunction = constructor.bind.apply(constructor, args); 
    return new factoryFunction(); 
} 

var d = applyToConstructor(Date, [2008, 10, 8, 00, 16, 34, 254]); 

Call es un poco más fácil

function callConstructor(constructor) { 
    var factoryFunction = constructor.bind.apply(constructor, arguments); 
    return new factoryFunction(); 
} 

var d = callConstructor(Date, 2008, 10, 8, 00, 16, 34, 254); 

Puede utilizar cualquiera de estos para crear funciones de fábrica:

var dateFactory = applyToConstructor.bind(null, Date) 
var d = dateFactory([2008, 10, 8, 00, 16, 34, 254]); 

o

var dateFactory = callConstructor.bind(null, Date) 
var d = dateFactory(2008, 10, 8, 00, 16, 34, 254); 

Funcionará con cualquier constructor, no solo con incorporaciones o constructores que puedan funcionar como funciones (como Date).

Sin embargo, sí requiere la función Ectascript 5 .bind. Las cuñas probablemente no funcionen correctamente.

Un enfoque diferente, más en el estilo de algunas de las otras respuestas es crear una versión de función del built in new. Esto no funcionará en todos los builtins (como Date).

function neu(constructor) { 
    // http://www.ecma-international.org/ecma-262/5.1/#sec-13.2.2 
    var instance = Object.create(constructor.prototype); 
    var result = constructor.apply(instance, Array.prototype.slice.call(arguments, 1)); 

    // The ECMAScript language types are Undefined, Null, Boolean, String, Number, and Object. 
    return (result !== null && typeof result === 'object') ? result : instance; 
} 

function Person(first, last) {this.first = first;this.last = last}; 
Person.prototype.hi = function(){console.log(this.first, this.last);}; 

var p = neu(Person, "Neo", "Anderson"); 

Y ahora, por supuesto que puede hacer .apply o .call o .bind en neu de forma normal.

Por ejemplo:

var personFactory = neu.bind(null, Person); 
var d = personFactory("Harry", "Potter"); 

Siento que la primera solución que doy es mejor sin embargo, ya que no depende de que la replicación correctamente la semántica de una orden interna y funciona correctamente con órdenes internas.

+5

Me sorprende que no hayas obtenido ningún voto al respecto. Las soluciones basadas en crear una función separada y cambiar su prototipo tienen la desventaja de cambiar el campo 'constructor', mientras que combinar' bind' con 'apply' permite mantenerlo. – mgol

+0

Esto está ordenado, pero no es compatible con IE8 y versiones posteriores. –

+1

Muy bien, ie8 no es un navegador ecmascript5 (que menciono). – kybernetikos

4

Otro enfoque, que requiere modificar el constructor real al que se llama, pero me parece más limpio que usar eval() o introducir una nueva función ficticia en la cadena de construcción ...Mantener su función como conthunktor

function conthunktor(Constructor) { 
    // Call the constructor 
    return Constructor.apply(null, Array.prototype.slice.call(arguments, 1)); 
} 

y modificar los constructores se llaman ...

function MyConstructor(a, b, c) { 
    if(!(this instanceof MyConstructor)) { 
    return new MyConstructor(a, b, c); 
    } 
    this.a = a; 
    this.b = b; 
    this.c = c; 
    // The rest of your constructor... 
} 

para que pueda probar:

var myInstance = conthunktor(MyConstructor, 1, 2, 3); 

var sum = myInstance.a + myInstance.b + myInstance.c; // sum is 6 
+0

Esto es lo mejor para mí, una solución limpia y elegante. – Delta

+0

La 'instancia de verificación de Constructor' es muy buena, pero impide la composición del constructor (es decir, constructores extensibles):' función Foo() {}; función Bar() {Foo.call (esto); } ' – Barney

+0

@Barney If' Bar.prototype' = 'Foo', la comprobación' instanceof' debería funcionar. – 1j01

1

hay una solución para este caso rehusable. Para cada clase a la que desee llamar con método apply o call, debe llamar antes a convertToAllowApply ('classNameInString'); La clase debe estar en el mismo Scoope o Scoope mundial (no intento enviar ns.className por ejemplo ...)

No es el código:

function convertToAllowApply(kName){ 
    var n = '\n', t = '\t'; 
    var scrit = 
     'var oldKlass = ' + kName + ';' + n + 
     kName + '.prototype.__Creates__ = oldKlass;' + n + 

     kName + ' = function(){' + n + 
      t + 'if(!(this instanceof ' + kName + ')){'+ n + 
       t + t + 'obj = new ' + kName + ';'+ n + 
       t + t + kName + '.prototype.__Creates__.apply(obj, arguments);'+ n + 
       t + t + 'return obj;' + n + 
      t + '}' + n + 
     '}' + n + 
     kName + '.prototype = oldKlass.prototype;'; 

    var convert = new Function(scrit); 

    convert(); 
} 

// USE CASE: 

myKlass = function(){ 
    this.data = Array.prototype.slice.call(arguments,0); 
    console.log('this: ', this); 
} 

myKlass.prototype.prop = 'myName is myKlass'; 
myKlass.prototype.method = function(){ 
    console.log(this); 
} 

convertToAllowApply('myKlass'); 

var t1 = myKlass.apply(null, [1,2,3]); 
console.log('t1 is: ', t1); 
3

Usando un constructor temporal parece ser la mejor solución si Object.create no está disponible.

Si Object.create está disponible, entonces usarlo es una opción mucho mejor. En Node.js, usando Object.create resulta en un código mucho más rápido. Aquí hay un ejemplo de cómo se puede utilizar Object.create:

function applyToConstructor(ctor, args) { 
    var new_obj = Object.create(ctor.prototype); 
    var ctor_ret = ctor.apply(new_obj, args); 

    // Some constructors return a value; make sure to use it! 
    return ctor_ret !== undefined ? ctor_ret: new_obj; 
} 

(. Obviamente, el argumento args es una lista de argumentos para aplicar)

que tenía un trozo de código que utilizó originalmente eval leer una pieza de datos creada por otra herramienta. (Sí, eval es malo.) Esto instanciaría un árbol de cientos a miles de elementos. Básicamente, el motor de JavaScript fue responsable de analizar y ejecutar un grupo de expresiones new ...(...). Convertí mi sistema para analizar una estructura JSON, lo que significa que tengo que hacer que mi código determine qué constructor llamar para cada tipo de objeto en el árbol. Cuando ejecuté el nuevo código en mi suite de pruebas, me sorprendió ver una disminución dramática relativa a la versión eval.

  1. Prueba suite con eval versión: 1 segundo.
  2. Suite de prueba con la versión JSON, utilizando el constructor temporal: 5 segundos.
  3. Suite de prueba con la versión JSON, usando Object.create: 1 segundo.

El conjunto de pruebas crea varios árboles. Calculé que mi función applytoConstructor se llamó unas 125,000 veces cuando se ejecuta el banco de pruebas.

3

En ECMAScript 6, se puede utilizar el operador de difusión para aplicar un constructor con la nueva palabra clave para una serie de argumentos:

var dateFields = [2014, 09, 20, 19, 31, 59, 999]; 
var date = new Date(...dateFields); 
console.log(date); // Date 2014-10-20T15:01:59.999Z 
+0

¡Agradable! 1. –

Cuestiones relacionadas