Vermeiden Sie neue Operatoren in JavaScript - der bessere Weg

16

Warnung: Dies ist ein langer Beitrag.

Lassen Sie es uns einfach halten. Ich möchte vermeiden, dass dem neuen Operator jedes Mal ein Präfix vorangestellt werden muss, wenn ich einen Konstruktor in JavaScript aufrufe. Das liegt daran, dass ich es oft vergesse und mein Code schlecht funktioniert.

Der einfache Weg, dies zu umgehen, ist ...

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

  // do your stuff...
}

Aber ich brauche dies, um die Variable Nr. Zu akzeptieren. von Argumenten wie diesem ...

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

Die erste unmittelbare Lösung scheint die "Apply" -Methode wie diese zu sein ...

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

  // do your stuff
}

Dies ist jedoch FALSCH - das neue Objekt wird an die applyMethode und NICHT an unseren Konstruktor übergeben arguments.callee.

Jetzt habe ich drei Lösungen gefunden. Meine einfache Frage ist: Welches scheint am besten zu sein. Oder wenn Sie eine bessere Methode haben, sagen Sie es.

Erstens : Verwenden Sie eval()diese Option, um dynamisch JavaScript-Code zu erstellen, der den Konstruktor aufruft.

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

Zweitens : Jedes Objekt hat eine __proto__Eigenschaft, die eine „geheime“ Verknüpfung zu seinem Prototypobjekt darstellt. Zum Glück ist diese Eigenschaft beschreibbar.

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

Drittens - Dies ähnelt der zweiten Lösung.

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 Die Lösung erscheint ungeschickt und bringt alle Probleme von "Eval Eval" mit sich.

  • __proto__ Die Lösung ist nicht standardisiert und wird vom "Great Browser of MISERY" nicht honoriert.

  • Die dritte Lösung scheint zu kompliziert.

Aber mit all den drei oben genannten Lösungen können wir so etwas tun, was wir sonst nicht können ...

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

Die obigen Lösungen geben uns also effektiv "echte" Konstruktoren, die die Variable Nr. von Argumenten und erfordern nicht new. Wie sehen Sie das?

- UPDATE -

Einige haben gesagt "wirf einfach einen Fehler". Meine Antwort lautet: Wir machen eine schwere App mit über 10 Konstruktoren, und ich denke, es wäre weitaus umständlicher, wenn jeder Konstruktor diesen Fehler "klug" handhaben könnte, ohne Fehlermeldungen auf die Konsole zu werfen.

Baumkodierer
quelle
2
oder werfen Sie einfach einen Fehler, wenn es schlecht ist, untersuchen Sie den Stacktrace und Sie können den Code reparieren
Ratschenfreak
2
Ich denke, dass diese Frage bei Stack Overflow oder Code Review besser gestellt werden sollte . Es scheint eher eine ziemlich code-zentrierte als eine konzeptionelle Frage zu sein.
Adam Lear
1
@greengit, anstatt einen Fehler zu werfen, benutze einen Jslint. Es wird Sie warnen , wenn Sie getan haben , Make()ohne newda Make aktiviert ist , und somit nimmt es ist ein Konstruktor
Raynos
1
Warten Sie also - suchen Sie nach einem besseren Weg, um dies zu erreichen, oder suchen Sie nur jemanden, der Ihnen Code gibt, damit Sie Objekte mit variablen Argumenten erstellen können, ohne auf den Sie verzichten können new? Denn wenn es das letztere ist, fragst du wahrscheinlich auf der falschen Seite. Wenn es das erstere ist, möchten Sie möglicherweise Vorschläge zur Verwendung neuer und zum Erkennen von Fehlern nicht so schnell verwerfen ... Wenn Ihre App wirklich "schwer" ist, ist das Letzte, was Sie wollen, ein überarbeiteter Konstruktionsmechanismus, der sie verlangsamt. new, bei allem Flackern ist es ziemlich flott.
Shog9
5
Ironischerweise ist der Versuch, mit Programmierfehlern 'klug' umzugehen, selbst für viele der 'schlechten Teile' von JavaScript verantwortlich.
Daniel Pratt

Antworten:

19

Erstens arguments.calleeist es in ES5 strikt veraltet, so dass wir es nicht verwenden. Die wirkliche Lösung ist ziemlich einfach.

Sie verwenden überhaupt nicht new.

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

Das ist ein richtiger Schmerz im Arsch, oder?

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

Das setzt natürlich ES5 voraus, aber jeder nutzt das ES5-Shim, oder?

Sie könnten auch an alternativen js OO-Mustern interessiert sein

Nebenbei können Sie Option zwei durch ersetzen

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

  return that;
}

Wenn Sie Ihr eigenes ES5- Object.createShim haben möchten , ist das ganz einfach

Object.create = function (proto) {
  var dummy = function () {};
  dummy.prototype = proto;
  return new dummy;
};
Raynos
quelle
Ja wahr - aber die ganze Magie hier ist wegen Object.create. Was ist mit Pre-ES5? ES5-Shim wird Object.createals DUBIOUS aufgelistet.
Treecoder
@greengit Wenn Sie es erneut lesen, gibt ES5 shim an, dass das zweite Argument für Object.create DUBIOUS ist. Der erste ist in Ordnung.
Raynos
1
Ja, das habe ich gelesen. Und ich denke (WENN) sie dort etwas benutzen __proto__, dann sind wir immer noch am selben Punkt. Da es vor ES5 keinen einfacheren Weg gibt, den Prototyp zu mutieren. Trotzdem wirkt Ihre Lösung äußerst elegant und zukunftsorientiert. +1 dafür. (Mein Abstimmungslimit ist erreicht)
treecoder
1
Danke @psr. Und @Raynos dein Object.createShim ist so ziemlich meine dritte Lösung, aber weniger kompliziert und sieht besser aus als meine natürlich.
Treecoder
36

Lassen Sie es uns einfach halten. Ich möchte vermeiden, dass dem neuen Operator jedes Mal ein Präfix vorangestellt werden muss, wenn ich einen Konstruktor in JavaScript aufrufe. Das liegt daran, dass ich es oft vergesse und mein Code schlecht funktioniert.

Die offensichtliche Antwort wäre, das newSchlüsselwort nicht zu vergessen .

Sie ändern die Struktur und Bedeutung der Sprache.

Was meiner Meinung nach und im Interesse der zukünftigen Betreuer Ihres Codes eine schreckliche Idee ist.

CaffGeek
quelle
9
+1 Es scheint seltsam, eine Sprache um die schlechten Codierungsgewohnheiten zu streiten. Sicherlich können einige Syntaxhervorhebungs- / Eincheckrichtlinien die Vermeidung fehleranfälliger Muster / wahrscheinlicher Tippfehler erzwingen.
Stoive
Wenn ich eine Bibliothek finden würde, bei der es allen Konstrukteuren egal ist, ob Sie sie verwenden newoder nicht, wäre das für mich wartbarer.
Jack
@Jack, aber es wird viel schwieriger und subtiler sein, Fehler zu finden. Schauen Sie sich einfach alle Fehler an, die durch Javascript "egal" verursacht wurden, wenn Sie die ;to end-Anweisungen einschließen . (Automatische Semikoloneinfügung)
CaffGeek
@CaffGeek "Javascript kümmert sich nicht um Semikolons" ist nicht korrekt - es gibt Fälle, in denen ein Semikolon verwendet und nicht verwendet wird, und es gibt Fälle, in denen dies nicht der Fall ist. Das ist das Problem. Die in der akzeptierten Antwort präsentierte Lösung ist genau das Gegenteil - mit ihr sind in allen Fällen die Verwendung newoder nicht semantisch identisch . Es gibt keine subtilen Fälle, in denen diese Invariante gebrochen ist. Das ist der Grund, warum es gut ist und warum Sie es verwenden möchten.
Jack
14

Die einfachste Lösung besteht darin new, sich einen Fehler zu merken und ihn zu werfen, um zu verdeutlichen, dass Sie ihn vergessen haben.

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

Was auch immer du tust, benutze es nicht eval. Ich würde es scheuen, Nicht-Standard-Eigenschaften zu verwenden, zum Beispiel, __proto__weil sie nicht dem Standard entsprechen und sich möglicherweise in ihrer Funktionalität ändern.

Josh K
quelle
Vermeiden .__proto__Sie es auf
jeden Fall
3

Ich habe tatsächlich einen Post darüber geschrieben. http://js-bits.blogspot.com/2010/08/constructors-without-using-new.html

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

Und Sie können es sogar verallgemeinern, damit Sie es nicht jedem Konstruktor hinzufügen müssen. Sie können das sehen, indem Sie meinen Beitrag besuchen

Haftungsausschluss Ich verwende dies nicht in meinem Code, ich habe es nur für den didaktischen Wert gepostet. Ich fand, dass das Vergessen von a newein leicht zu entdeckender Fehler ist. Wie andere glaube ich nicht, dass wir dies für die meisten Codes tatsächlich brauchen. Es sei denn, Sie schreiben eine Bibliothek zum Erstellen einer JS-Vererbung. In diesem Fall könnten Sie diese von einer einzigen Stelle aus verwenden und würden bereits einen anderen Ansatz als die direkte Vererbung verwenden.

Juan Mendes
quelle
Möglicherweise versteckter Fehler: Wenn ich var x = new Ctor();und dann später x als habe thisund tue var y = Ctor();, würde sich dies nicht wie erwartet verhalten.
Luiscubal
@luiscubal Du bist dir nicht sicher, was du sagst "Habe später x als this". Kannst du vielleicht eine Jsfiddle posten, um das potenzielle Problem zu zeigen?
Juan Mendes
Ihr Code ist ein bisschen robuster als ich ursprünglich dachte, aber ich habe es geschafft, ein (etwas verschlungenes,
luiscubal
@ Luiscubal Ich verstehe deinen Standpunkt, aber es ist wirklich ein verschlungener. Sie gehen davon aus, dass es in Ordnung ist, anzurufen Ctor.call(ctorInstance, 'value'). Ich sehe kein gültiges Szenario für das, was Sie tun. Um ein Objekt zu konstruieren, verwenden Sie entweder var x = new Ctor(val)oder var y=Ctor(val). Selbst wenn es ein gültiges Szenario gab, ist meine Behauptung, dass Sie Konstruktoren ohne Verwendung haben können new Ctor, nicht, dass Sie Konstruktoren haben können, die unter Verwendung von funktionieren. Ctor.callSiehe jsfiddle.net/JHNcR/2
Juan Mendes
0

Wie wäre es damit?

/* 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: Ich habe vergessen hinzuzufügen:

"Wenn Ihre App wirklich" schwer "ist, ist das Letzte, was Sie wollen, ein überarbeiteter Konstruktionsmechanismus, der sie verlangsamt."

Ich stimme voll und ganz zu - wenn Sie ein "Ding" oben ohne das "neue" Schlüsselwort erstellen, ist es langsamer / schwerer als mit diesem Schlüsselwort. Fehler sind dein Freund, weil sie dir sagen, was los ist. Außerdem teilen sie Ihren Mitentwicklern mit, was sie falsch machen.

Encoder
quelle
Das Erfinden von Werten für fehlende Konstruktorargumente ist fehleranfällig. Wenn sie fehlen, lassen Sie die Eigenschaften undefiniert. Wenn sie wichtig sind, werfen Sie einen Fehler, wenn sie fehlen. Was wäre, wenn prop1 blöd wäre? Das Testen von "Nö" auf Wahrhaftigkeit wird eine Fehlerquelle sein.
JBRWilkinson