Evitare il nuovo operatore in JavaScript: il modo migliore

16

Avviso: questo è un post lungo.

Manteniamolo semplice. Voglio evitare di dover prefisso il nuovo operatore ogni volta che chiamo un costruttore in JavaScript. Questo perché tendo a dimenticarlo, e il mio codice si rovina male.

Il modo semplice per aggirare questo è ...

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

  // do your stuff...
}

Ma ho bisogno che accetti la variabile no. di argomenti, come questo ...

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

La prima soluzione immediata sembra essere il metodo "applica" come questo ...

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

  // do your stuff
}

Questo è WRONG comunque - il nuovo oggetto è passato al metodo apply e NON al nostro costruttore arguments.callee .

Ora ho trovato tre soluzioni. La mia semplice domanda è: quale sembra migliore. Oppure, se hai un metodo migliore, dillo.

Prima - usa eval() per creare dinamicamente il codice JavaScript che chiama il costruttore.

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...
}

Secondo - Ogni oggetto ha una proprietà __proto__ che è un collegamento "segreto" al suo oggetto prototipo. Fortunatamente questa proprietà è scrivibile.

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

Terzo - Questo è qualcosa di simile alla seconda soluzione.

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;
    La soluzione
  • eval sembra maldestra e viene fornita con tutti i problemi di "valutazione del male".

  • __proto__ solution non è standard e il "Great Browser of mIsERY" non lo onora.

  • La terza soluzione sembra eccessivamente complicata.

Ma con tutte le tre soluzioni precedenti, possiamo fare qualcosa di simile, che non possiamo altrimenti ...

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

Così efficacemente le soluzioni di cui sopra ci danno costruttori "veri" che accettano la variabile no. di argomenti e non richiedono new . Qual è la tua opinione su questo.

- UPDATE -

Alcuni hanno detto "basta lanciare un errore". La mia risposta è: stiamo facendo un'app pesante con 10+ costruttori e penso che sarebbe molto più maneggevole se ogni costruttore potesse "abilmente" gestire quell'errore senza lanciare messaggi di errore sulla console.

    
posta treecoder 09.11.2011 - 17:55
fonte

5 risposte

19

Prima di tutto arguments.callee è deprecato in ES5 strict quindi non lo usiamo. La vera soluzione è piuttosto semplice.

Non usi affatto new .

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

È proprio un bel rompicoglioni vero?

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

Ora ovviamente questo richiede ES5, ma tutti usano ES5-shim giusto?

Potresti anche essere interessato a modelli alternativi di OO di js

Come parte puoi sostituire l'opzione due con

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

  return that;
}

Nel caso in cui desideri il tuo ES5% diObject.create shim, allora è davvero facile

Object.create = function (proto) {
  var dummy = function () {};
  dummy.prototype = proto;
  return new dummy;
};
    
risposta data 09.11.2011 - 18:06
fonte
36

Let's keep it simple. I want to avoid having to prefix the new operator every time I call a constructor in JavaScript. This is because I tend to forget it, and my code screws up badly.

La risposta ovvia sarebbe non dimenticare la parola chiave new .

Stai cambiando la struttura e il significato della lingua.

Quale, a mio parere, e per il bene dei futuri manutentori del tuo codice, è un'idea orribile.

    
risposta data 09.11.2011 - 18:16
fonte
14

La soluzione più semplice è ricordare new e lanciare un errore per rendere evidente che hai dimenticato.

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

Qualunque cosa tu faccia, non usare eval . Vorrei evitare di usare proprietà non standard come __proto__ in particolare perché sono non standard e la loro funzionalità potrebbe cambiare.

    
risposta data 09.11.2011 - 18:13
fonte
3

In realtà ho scritto un post su questo. link

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

E puoi anche generalizzarlo in modo da non doverlo aggiungere all'inizio di ogni costruttore. Puoi vederlo visitando il mio post

Disclaimer Non lo uso nel mio codice, l'ho solo pubblicato per il valore didattico. Ho scoperto che dimenticare un new è un bug facile da individuare. Come gli altri, non penso che ne abbiamo davvero bisogno per la maggior parte del codice. A meno che non si stia scrivendo una libreria per la creazione dell'ereditarietà JS, nel qual caso è possibile utilizzare da un'unica posizione e si utilizzerebbe già un approccio diverso rispetto all'ereditarietà diretta.

    
risposta data 15.06.2012 - 00:29
fonte
0

Come su questo?

/* 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' */

EDIT: ho dimenticato di aggiungere:

"Se la tua app è veramente 'pesante', l'ultima cosa che vuoi è un meccanismo di costruzione troppo duro per rallentarlo"

Sono assolutamente d'accordo: quando si crea una "cosa" in alto senza la parola chiave "nuova", è più lento / più pesante che con essa. Gli errori sono tuoi amici, perché ti dicono cosa c'è che non va. Inoltre, dicono ai tuoi colleghi sviluppatori che cosa stanno facendo

    
risposta data 30.11.2011 - 00:58
fonte

Leggi altre domande sui tag