Evitare il nuovo operatore in JavaScript: il modo migliore


16

Attenzione: questo è un post lungo.

Manteniamolo semplice. Voglio evitare di dover aggiungere il prefisso al 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 è questo ...

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

  // do your stuff...
}

Ma ho bisogno di questo per accettare 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
}

Tuttavia, questo è SBAGLIATO: il nuovo oggetto viene passato al applymetodo e NON al nostro costruttore arguments.callee.

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

Primo : utilizzare eval()per creare dinamicamente 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 __proto__proprietà 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 : è 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;

  • eval la soluzione sembra goffa e presenta tutti i problemi di "malvagia valutazione".

  • __proto__ la soluzione non è standard e il "Grande Browser di mIsERY" non lo onora.

  • La terza soluzione sembra eccessivamente complicata.

Ma con tutte queste tre soluzioni, possiamo fare qualcosa del genere, che altrimenti non potremmo ...

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 n. di argomenti e non richiedono new. Cosa ne pensi di questo.

-- AGGIORNARE --

Alcuni hanno detto "basta lanciare un errore". La mia risposta è: stiamo realizzando un'app pesante con oltre 10 costruttori e penso che sarebbe molto più utile se ogni costruttore potesse "intelligentemente" gestire l'errore senza inviare messaggi di errore sulla console.


2
o semplicemente genera un errore quando non va bene esamina lo stacktrace e puoi correggere il codice
ratchet maniaco

2
Penso che questa domanda sarebbe meglio porre su StackTranslate.it o Code Review . Sembra essere piuttosto incentrato sul codice piuttosto che una domanda concettuale.
Adam Lear

1
@greengit piuttosto che lanciare un errore usa un jslint. Ti avvertirà se lo facessi Make()senza newperché Make è in maiuscolo e quindi presume che sia un costruttore
Raynos

1
Quindi aspetta - stai cercando un modo migliore per raggiungere questo obiettivo o stai semplicemente cercando qualcuno che ti dia codice in modo da poter avere la creazione di oggetti con argomento variabile senza new? Perché se è quest'ultimo, probabilmente stai chiedendo sul sito sbagliato. Se è il primo, potresti voler non respingere i suggerimenti sull'utilizzo di nuovi e rilevare errori così rapidamente ... Se la tua app è veramente "pesante", l'ultima cosa che desideri è un meccanismo di costruzione rovinato per rallentarlo. new, nonostante tutto il flack che ottiene, è piuttosto veloce.
Shog9

5
Ironia della sorte, il tentativo di gestire "in modo intelligente" gli errori del programmatore è esso stesso responsabile di molte delle "parti difettose" di JavaScript.
Daniel Pratt,

Risposte:


19

Innanzitutto arguments.calleeè deprecato in ES5 rigoroso, quindi non lo usiamo. La vera soluzione è piuttosto semplice.

Non usi newaffatto.

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

Questo è un dolore nel culo giusto?

Provare 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 lo shim ES5 giusto?

Potresti anche essere interessato a modelli js OO alternativi

A 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 Object.createspessore ES5 , è davvero facile

Object.create = function (proto) {
  var dummy = function () {};
  dummy.prototype = proto;
  return new dummy;
};

Sì vero - ma tutta la magia qui è dovuta a Object.create. Che dire di pre ES5? Elenchi ES5-Shim Object.createcome DUBIOUS.
treecoder

@greengit se lo rileggi, lo shim ES5 afferma che il secondo argomento di Object.create è DUBIOUS. il primo va bene.
Raynos,

1
Sì, l'ho letto. E penso che (IF) stiano usando qualcosa del __proto__genere lì, quindi siamo ancora sullo stesso punto. Perché prima dell'ES5 non c'è modo più semplice per mutare il prototipo. Ma comunque, la tua soluzione sembra più elegante e lungimirante. +1 per quello. (il mio limite di voto è raggiunto)
treecoder

1
Grazie @psr. E @Raynos il tuo Object.createspessore è praticamente la mia terza soluzione, ma meno complicata e più bella della mia ovviamente.
treecoder

37

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

La risposta ovvia sarebbe non dimenticare la newparola chiave.

Stai cambiando la struttura e il significato della lingua.

Il che, secondo me, e per il bene dei futuri manutentori del tuo codice, è un'idea orribile.


9
+1 Sembra strano strangolare una lingua attorno alle cattive abitudini di programmazione. Sicuramente alcune politiche di evidenziazione / check-in della sintassi possono imporre l'elusione di schemi inclini a bug / possibili errori di battitura.
Stoive il

Se newtrovassi una biblioteca in cui a tutti i costruttori non importava se tu lo usassi o no, lo troverei più mantenibile.
Jack,

@ Jack, ma introdurrà molto più difficile e sottile per trovare bug. Basta dare un'occhiata a tutti i bug causati da javascript "non preoccuparsi" se si includono le ;dichiarazioni finali. (Inserimento automatico punto e virgola)
CaffGeek,

@CaffGeek "Javascript non si preoccupa dei punti e virgola" non è preciso - ci sono casi in cui l'uso di un punto e virgola e il suo mancato utilizzo sono leggermente diversi, e ci sono casi in cui non lo sono. Questo è il problema. La soluzione presentata nella risposta accettata è esattamente l'opposto - con essa, in tutti i casi l' utilizzo newo meno sono semanticamente identici . Non ci sono casi sottili in cui questo invariante è rotto. Ecco perché è buono e perché dovresti usarlo.
Jack,

14

La soluzione più semplice è semplicemente ricordare newe 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 nello __proto__specifico perché sono non standard e la loro funzionalità può cambiare.


Evidentemente evitatelo .__proto__è il diavolo
Raynos,

3

In realtà ho scritto un post su questo. 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 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 pubblicato solo per il valore didattico. Ho scoperto che dimenticare a newè un bug facile da individuare. Come altri, non penso che ne abbiamo davvero bisogno per la maggior parte del codice. A meno che tu non stia scrivendo una libreria per la creazione dell'ereditarietà JS, nel qual caso potresti utilizzare da un unico posto e utilizzeresti già un approccio diverso rispetto all'ereditarietà diretta.


Bug potenzialmente nascosto: se avessi var x = new Ctor();e poi avessi x come thise cosa fare var y = Ctor();, questo non si comporterebbe come previsto.
luiscubal,

@luiscubal Non sei sicuro di quello che stai dicendo "dopo hai x as this", puoi forse pubblicare un jsfiddle per mostrare il potenziale problema?
Juan Mendes,

Il tuo codice è un po 'più robusto di quanto pensassi inizialmente, ma sono riuscito a trovare un esempio (un po' contorto ma ancora valido): jsfiddle.net/JHNcR/1
luiscubal

@luiscubal Vedo il tuo punto ma è davvero contorto. Stai assumendo che sia OK chiamare Ctor.call(ctorInstance, 'value'). Non vedo uno scenario valido per quello che stai facendo. Per costruire un oggetto, utilizzare var x = new Ctor(val)o var y=Ctor(val). Anche se c'era uno scenario valido, la mia affermazione è che puoi avere costruttori senza usare new Ctor, non che puoi avere costruttori che funzionano usando Ctor.callVedi jsfiddle.net/JHNcR/2
Juan Mendes

0

Cosa ne pensi di 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 rovinato per rallentarla"

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


Inventare valori per argomenti di costruttore mancanti è soggetto a errori. Se mancano, lasciare le proprietà non definite. Se sono essenziali, genera un errore se mancano. E se prop1 dovesse essere bool? Testare "no" per la verità sarà una fonte di errori.
JBR Wilkinson,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.