Evitar un nuevo operador en JavaScript: la mejor manera

16

Advertencia: esta es una publicación larga.

Hagámoslo simple. Quiero evitar tener que prefijar el nuevo operador cada vez que llamo a un constructor en JavaScript. Esto se debe a que tiendo a olvidarlo, y mi código se arruina mal.

La forma simple de evitar esto es esto ...

function Make(x) {
  if ( !(this instanceof arguments.callee) )
  return new arguments.callee(x);

  // do your stuff...
}

Pero, necesito esto para aceptar la variable no. de argumentos como este ...

m1 = Make();
m2 = Make(1,2,3);
m3 = Make('apple', 'banana');

La primera solución inmediata parece ser el método 'aplicar' como este ...

function Make() {
  if ( !(this instanceof arguments.callee) )
    return new arguments.callee.apply(null, arguments);

  // do your stuff
}

Sin embargo, esto es INCORRECTO: el nuevo objeto se pasa al applymétodo y NO a nuestro constructor arguments.callee.

Ahora, he encontrado tres soluciones. Mi simple pregunta es: cuál parece mejor. O, si tiene un método mejor, dígalo.

Primero , use eval()para crear dinámicamente código JavaScript que llame al constructor.

function Make(/* ... */) {
  if ( !(this instanceof arguments.callee) ) {
    // collect all the arguments
    var arr = [];
    for ( var i = 0; arguments[i]; i++ )
      arr.push( 'arguments[' + i + ']' );

    // create code
    var code = 'new arguments.callee(' + arr.join(',') + ');';

    // call it
    return eval( code );
  }

  // do your stuff with variable arguments...
}

Segundo : cada objeto tiene una __proto__propiedad que es un enlace 'secreto' a su objeto prototipo. Afortunadamente, esta propiedad se puede escribir.

function Make(/* ... */) {
  var obj = {};

  // do your stuff on 'obj' just like you'd do on 'this'
  // use the variable arguments here

  // now do the __proto__ magic
  // by 'mutating' obj to make it a different object

  obj.__proto__ = arguments.callee.prototype;

  // must return obj
  return obj;
}

Tercero : esto es algo similar a la segunda solución.

function Make(/* ... */) {
  // we'll set '_construct' outside
  var obj = new arguments.callee._construct();

  // now do your stuff on 'obj' just like you'd do on 'this'
  // use the variable arguments here

  // you have to return obj
  return obj;
}

// now first set the _construct property to an empty function
Make._construct = function() {};

// and then mutate the prototype of _construct
Make._construct.prototype = Make.prototype;

  • eval La solución parece torpe y viene con todos los problemas de "evaluación malvada".

  • __proto__ la solución no es estándar y el "Gran navegador de MISERY" no la cumple.

  • La tercera solución parece demasiado complicada.

Pero con las tres soluciones anteriores, podemos hacer algo como esto, de lo contrario no podemos ...

m1 = Make();
m2 = Make(1,2,3);
m3 = Make('apple', 'banana');

m1 instanceof Make; // true
m2 instanceof Make; // true
m3 instanceof Make; // true

Make.prototype.fire = function() {
  // ...
};

m1.fire();
m2.fire();
m3.fire();

Entonces, efectivamente, las soluciones anteriores nos dan constructores "verdaderos" que aceptan la variable no. de argumentos y no requieren new. Cuál es su opinión sobre esto.

- ACTUALIZACIÓN -

Algunos han dicho "solo arroja un error". Mi respuesta es: estamos haciendo una aplicación pesada con más de 10 constructores y creo que sería mucho más manejable si cada constructor pudiera "inteligentemente" manejar ese error sin lanzar mensajes de error en la consola.

codificador de árboles
fuente
2
o simplemente lanzar un error cuando es malo examinar el StackTrace y se puede corregir el código
monstruo de trinquete
2
Creo que esta pregunta se haría mejor en Stack Overflow o Code Review . Parece ser bastante centrado en el código en lugar de una pregunta conceptual.
Adam Lear
1
@greengit en lugar de arrojar un error, use un jslint. Se le avisará si se hizo Make()sin newComo make es capitalizado y por lo tanto se asume que es un constructor
Raynos
1
Entonces, espere: ¿está buscando una mejor manera de lograr esto, o simplemente está buscando a alguien que le dé el código para que pueda tener la creación de objetos de argumento variable sin new? Porque si es lo último, probablemente estés preguntando en el sitio equivocado. Si es lo primero, es posible que no desee descartar sugerencias sobre el uso de nuevos y la detección de errores tan rápido ... Si su aplicación es realmente "pesada", lo último que desea es algún mecanismo de construcción sobrecargado para ralentizarlo. new, a pesar de todo el flack que recibe, es bastante rápido.
Shog9
55
Irónicamente, tratar de manejar 'inteligentemente' los errores del programador es en sí mismo responsable de muchas de las 'partes malas' de JavaScript.
Daniel Pratt

Respuestas:

19

En primer lugar, arguments.calleeestá obsoleto en ES5 estricto, por lo que no lo usamos. La verdadera solución es bastante simple.

No lo usas newen absoluto.

var Make = function () {
  if (Object.getPrototypeOf(this) !== Make.prototype) {
    var o = Object.create(Make.prototype);
    o.constructor.apply(o, arguments);
    return o;
  }
  ...
}

Eso es un dolor en el culo ¿verdad?

Tratar enhance

var Make = enhance(function () {
  ...
});

var enhance = function (constr) {
  return function () {
    if (Object.getPrototypeOf(this) !== constr.prototype) {
      var o = Object.create(constr.prototype);
      constr.apply(o, arguments);
      return o;
    }
    return constr.apply(this, arguments);
  }
}

Ahora, por supuesto, esto requiere ES5, pero todos usan la cuña ES5 ¿verdad?

Usted también podría estar interesado en patrones alternativos de js OO

Como aparte, puede reemplazar la opción dos con

var Make = function () {
  var that = Object.create(Make.prototype);
  // use that 

  return that;
}

En caso de que quieras tu propia Object.createcuña ES5, entonces es realmente fácil

Object.create = function (proto) {
  var dummy = function () {};
  dummy.prototype = proto;
  return new dummy;
};
Raynos
fuente
Sí, cierto, pero toda la magia aquí se debe a Object.create. ¿Qué pasa con pre ES5? ES5-Shim Object.createaparece como DUBIOSO.
Treecoder
@greengit si lo vuelve a leer, la cuña ES5 indica que el segundo argumento para Object.create es DUBIOSO. El primero está bien.
Raynos
1
Sí, lo leí. Y creo que (SI) están usando algún tipo de __proto__cosa allí, entonces todavía estamos en el mismo punto. Debido a que antes de ES5 NO hay una forma más fácil de mutar el prototipo. Pero de todos modos, su solución parece más elegante y prospectiva. +1 por eso. (se alcanza mi límite de votación)
treecoder
1
Gracias @psr. Y @Raynos tu Object.createcuña es más o menos mi tercera solución, pero menos complicada y más atractiva que la mía, por supuesto.
Treecoder
37

Hagámoslo simple. Quiero evitar tener que prefijar el nuevo operador cada vez que llamo a un constructor en JavaScript. Esto se debe a que tiendo a olvidarlo, y mi código se arruina mal.

La respuesta obvia sería no olvidar la newpalabra clave.

Estás cambiando la estructura y el significado del lenguaje.

Lo cual, en mi opinión, y por el bien de los futuros mantenedores de su código, es una idea horrible.

CaffGeek
fuente
9
+1 Parece extraño discutir un lenguaje en torno a los malos hábitos de codificación. Seguramente alguna política de resaltado / verificación de sintaxis puede obligar a evitar patrones propensos a errores / errores tipográficos probables.
Stoive
Si encontraba una biblioteca donde a todos los constructores no les importaba si la usabas newo no, la encontraría más fácil de mantener.
Jack
@Jack, pero introducirá errores mucho más difíciles y sutiles de encontrar. Simplemente mire a su alrededor todos los errores causados ​​por javascript "no importa" si incluye las ;declaraciones de finalización. (Inserción automática de punto y coma)
CaffGeek
@CaffGeek "Javascript no se preocupa por el punto y coma" no es exacto: hay casos en los que usar un punto y coma y no usarlo son sutilmente diferentes, y hay casos en los que no lo son. Ese es el problema. La solución presentada en la respuesta aceptada es precisamente lo contrario: con ella, en todos los casos, usar newo no es semánticamente idéntico . No hay ningún caso sutiles en el que se ha roto esta invariante. Por eso es bueno y por qué querrías usarlo.
Jack
14

La solución más fácil es recordar newy lanzar un error para que sea obvio que lo olvidó.

if (Object.getPrototypeOf(this) !== Make.prototype) {
    throw new Error('Remember to call "new"!');
}

Hagas lo que hagas, no lo uses eval. Me gustaría evitar el uso de propiedades no estándar como __proto__específicamente porque no son estándar y su funcionalidad puede cambiar.

Josh K
fuente
Evítalo desafiantemente .__proto__es el diablo
Raynos
3

De hecho, escribí una publicación sobre esto. http://js-bits.blogspot.com/2010/08/constructors-without-using-new.html

function Ctor() {
    if (!(this instanceof Ctor) {
        return new Ctor(); 
    }
    // regular construction code
}

E incluso puede generalizarlo para no tener que agregarlo a la parte superior de cada constructor. Puedes ver eso visitando mi publicación

Descargo de responsabilidad No uso esto en mi código, solo lo publiqué por el valor didáctico. Descubrí que olvidar a newes un error fácil de detectar. Como otros, no creo que realmente necesitemos esto para la mayoría del código. A menos que esté escribiendo una biblioteca para crear herencia JS, en cuyo caso podría usarla desde un solo lugar y ya estaría usando un enfoque diferente al de la herencia directa.

Juan Mendes
fuente
Error potencialmente oculto: si tengo var x = new Ctor();y luego tengo x as thisy do var y = Ctor();, esto no se comportaría como se esperaba.
luiscubal
@luiscubal No estoy seguro de lo que está diciendo "más tarde tenga x como this", ¿puede publicar un jsfiddle para mostrar el problema potencial?
Juan Mendes
Su código es un poco más robusto de lo que inicialmente pensé, pero logré encontrar un ejemplo (algo complicado pero aún válido): jsfiddle.net/JHNcR/1
luiscubal
@luiscubal Veo tu punto, pero realmente es complicado. Está asumiendo que está bien llamar Ctor.call(ctorInstance, 'value'). No veo un escenario válido para lo que estás haciendo. Para construir un objeto, use var x = new Ctor(val)o var y=Ctor(val). Incluso si hubiera un escenario válido, mi afirmación es que puede tener constructores sin usar new Ctor, no que puede tener constructores que funcionen usando Ctor.callVer jsfiddle.net/JHNcR/2
Juan Mendes
0

¿Qué tal esto?

/* thing maker! it makes things! */
function thing(){
    if (!(this instanceof thing)){
        /* call 'new' for the lazy dev who didn't */
        return new thing(arguments, "lazy");
    };

    /* figure out how to use the arguments object, based on the above 'lazy' flag */
    var args = (arguments.length > 0 && arguments[arguments.length - 1] === "lazy") ? arguments[0] : arguments;

    /* set properties as needed */
    this.prop1 = (args.length > 0) ? args[0] : "nope";
    this.prop2 = (args.length > 1) ? args[1] : "nope";
};

/* create 3 new things (mixed 'new' and 'lazy') */
var myThing1 = thing("woo", "hoo");
var myThing2 = new thing("foo", "bar");
var myThing3 = thing();

/* test your new things */
console.log(myThing1.prop1); /* outputs 'woo' */
console.log(myThing1.prop2); /* outputs 'hoo' */

console.log(myThing2.prop1); /* outputs 'foo' */
console.log(myThing2.prop2); /* outputs 'bar' */

console.log(myThing3.prop1); /* outputs 'nope' */
console.log(myThing3.prop2); /* outputs 'nope' */

EDITAR: Olvidé agregar:

"Si su aplicación es realmente 'pesada', lo último que desea es algún mecanismo de construcción sobrecargado para ralentizarla"

Estoy totalmente de acuerdo: al crear 'cosa' anterior sin la palabra clave 'nueva', es más lenta / más pesada que con ella. Los errores son tus amigos, porque te dicen lo que está mal. Además, le dicen a sus compañeros desarrolladores qué están haciendo mal.

codificador
fuente
Inventar valores para argumentos de constructor faltantes es propenso a errores. Si faltan, deje las propiedades sin definir. Si son esenciales, arroje un error si faltan. ¿Qué pasa si se suponía que prop1 era bool? Probar "no" para verificar la veracidad será una fuente de errores.
JBRWilkinson