Es gibt zwei Modelle zum Implementieren von Klassen und Instanzen in JavaScript: das Prototyping und das Schließen. Beide haben Vor- und Nachteile, und es gibt viele erweiterte Variationen. Viele Programmierer und Bibliotheken haben unterschiedliche Ansätze und Dienstprogramme zur Klassenhandhabung, um einige der hässlicheren Teile der Sprache zu dokumentieren.
Das Ergebnis ist, dass Sie in gemischter Gesellschaft eine Mischung aus Metaklassen haben, die sich alle leicht unterschiedlich verhalten. Was noch schlimmer ist, das meiste JavaScript-Tutorial-Material ist schrecklich und bietet eine Art Zwischenkompromiss, um alle Grundlagen abzudecken, was Sie sehr verwirrt. (Wahrscheinlich ist der Autor auch verwirrt. Das Objektmodell von JavaScript unterscheidet sich stark von den meisten Programmiersprachen und ist an vielen Stellen geradezu schlecht gestaltet.)
Beginnen wir mit dem Prototyp . Dies ist die JavaScript-native Version, die Sie erhalten können: Es gibt ein Minimum an Overhead-Code, und instanceof funktioniert mit Instanzen dieser Art von Objekten.
function Shape(x, y) {
this.x= x;
this.y= y;
}
Wir können der erstellten Instanz Methoden hinzufügen, indem wir new Shape
sie in die prototype
Suche dieser Konstruktorfunktion schreiben :
Shape.prototype.toString= function() {
return 'Shape at '+this.x+', '+this.y;
};
Nun zur Unterklasse, soweit Sie das nennen können, was JavaScript für die Unterklasse tut. Wir tun dies, indem wir diese seltsame magische prototype
Eigenschaft vollständig ersetzen :
function Circle(x, y, r) {
Shape.call(this, x, y); // invoke the base class's constructor function to take co-ords
this.r= r;
}
Circle.prototype= new Shape();
bevor Sie Methoden hinzufügen:
Circle.prototype.toString= function() {
return 'Circular '+Shape.prototype.toString.call(this)+' with radius '+this.r;
}
Dieses Beispiel wird funktionieren und Sie werden Code wie diesen in vielen Tutorials sehen. Aber Mann, das new Shape()
ist hässlich: Wir instanziieren die Basisklasse, obwohl keine tatsächliche Form erstellt werden soll. Es passiert Arbeit in diesem einfachen Fall , weil JavaScript so schlampig ist: es erlaubt Null Argumente wobei in diesem Fall übergeben, werden x
und y
wird undefined
und ist mit dem Prototyp des zugewiesen this.x
undthis.y
. Wenn die Konstruktorfunktion etwas Komplizierteres tun würde, würde sie flach auf das Gesicht fallen.
Wir müssen also einen Weg finden, ein Prototypobjekt zu erstellen, das die gewünschten Methoden und anderen Elemente auf Klassenebene enthält, ohne die Konstruktorfunktion der Basisklasse aufzurufen. Dazu müssen wir mit dem Schreiben des Hilfecodes beginnen. Dies ist der einfachste Ansatz, den ich kenne:
function subclassOf(base) {
_subclassOf.prototype= base.prototype;
return new _subclassOf();
}
function _subclassOf() {};
Dadurch werden die Mitglieder der Basisklasse in ihrem Prototyp an eine neue Konstruktorfunktion übertragen, die nichts tut, und dann dieser Konstruktor verwendet. Jetzt können wir einfach schreiben:
function Circle(x, y, r) {
Shape.call(this, x, y);
this.r= r;
}
Circle.prototype= subclassOf(Shape);
statt der new Shape()
Ungerechtigkeit. Wir haben jetzt einen akzeptablen Satz von Grundelementen für erstellte Klassen.
Es gibt einige Verbesserungen und Erweiterungen, die wir bei diesem Modell berücksichtigen können. Zum Beispiel ist hier eine syntaktische Zuckerversion:
Function.prototype.subclass= function(base) {
var c= Function.prototype.subclass.nonconstructor;
c.prototype= base.prototype;
this.prototype= new c();
};
Function.prototype.subclass.nonconstructor= function() {};
...
function Circle(x, y, r) {
Shape.call(this, x, y);
this.r= r;
}
Circle.subclass(Shape);
Jede Version hat den Nachteil, dass die Konstruktorfunktion nicht vererbt werden kann, wie dies in vielen Sprachen der Fall ist. Selbst wenn Ihre Unterklasse dem Konstruktionsprozess nichts hinzufügt, muss sie daran denken, den Basiskonstruktor mit den von der Basis gewünschten Argumenten aufzurufen. Dies kann leicht automatisiert werden apply
, aber Sie müssen immer noch schreiben:
function Point() {
Shape.apply(this, arguments);
}
Point.subclass(Shape);
Eine übliche Erweiterung besteht also darin, das Initialisierungsmaterial in seine eigene Funktion und nicht in den Konstruktor selbst aufzuteilen. Diese Funktion kann dann ganz gut von der Basis erben:
function Shape() { this._init.apply(this, arguments); }
Shape.prototype._init= function(x, y) {
this.x= x;
this.y= y;
};
function Point() { this._init.apply(this, arguments); }
Point.subclass(Shape);
// no need to write new initialiser for Point!
Jetzt haben wir für jede Klasse das gleiche Konstruktorfunktions-Boilerplate. Vielleicht können wir das in eine eigene Hilfsfunktion verschieben, damit wir es nicht weiter tippen müssen, anstatt es beispielsweise Function.prototype.subclass
umzudrehen und die Funktion der Basisklasse Unterklassen ausspucken zu lassen:
Function.prototype.makeSubclass= function() {
function Class() {
if ('_init' in this)
this._init.apply(this, arguments);
}
Function.prototype.makeSubclass.nonconstructor.prototype= this.prototype;
Class.prototype= new Function.prototype.makeSubclass.nonconstructor();
return Class;
};
Function.prototype.makeSubclass.nonconstructor= function() {};
...
Shape= Object.makeSubclass();
Shape.prototype._init= function(x, y) {
this.x= x;
this.y= y;
};
Point= Shape.makeSubclass();
Circle= Shape.makeSubclass();
Circle.prototype._init= function(x, y, r) {
Shape.prototype._init.call(this, x, y);
this.r= r;
};
... was ein bisschen mehr wie andere Sprachen aussieht, wenn auch mit etwas ungeschickterer Syntax. Wenn Sie möchten, können Sie einige zusätzliche Funktionen hinzufügen. Vielleicht möchten Sie makeSubclass
einen Klassennamen nehmen und sich daran erinnern und einen Standard toString
verwenden, der ihn verwendet. Vielleicht möchten Sie den Konstruktor erkennen lassen, wenn er versehentlich ohne das aufgerufen wurdenew
Operator (was sonst oft zu einem sehr nervigen Debugging führen würde):
Function.prototype.makeSubclass= function() {
function Class() {
if (!(this instanceof Class))
throw('Constructor called without "new"');
...
Vielleicht möchten Sie alle neuen Mitglieder übergeben und makeSubclass
sie dem Prototyp hinzufügen, damit Sie Class.prototype...
nicht so viel schreiben müssen. Viele Klassensysteme tun dies, z.
Circle= Shape.makeSubclass({
_init: function(x, y, z) {
Shape.prototype._init.call(this, x, y);
this.r= r;
},
...
});
Es gibt viele potenzielle Merkmale, die Sie in einem Objektsystem für wünschenswert halten könnten, und niemand stimmt einer bestimmten Formel wirklich zu.
Der Verschlussweg also . Dies vermeidet die Probleme der prototypbasierten Vererbung von JavaScript, indem überhaupt keine Vererbung verwendet wird. Stattdessen:
function Shape(x, y) {
var that= this;
this.x= x;
this.y= y;
this.toString= function() {
return 'Shape at '+that.x+', '+that.y;
};
}
function Circle(x, y, r) {
var that= this;
Shape.call(this, x, y);
this.r= r;
var _baseToString= this.toString;
this.toString= function() {
return 'Circular '+_baseToString(that)+' with radius '+that.r;
};
};
var mycircle= new Circle();
Jetzt hat jede einzelne Instanz von Shape
eine eigene Kopie dertoString
Methode (und aller anderen Methoden oder anderen Klassenmitglieder, die wir hinzufügen).
Das Schlechte an jeder Instanz, die eine eigene Kopie jedes Klassenmitglieds hat, ist, dass sie weniger effizient ist. Wenn Sie mit einer großen Anzahl von Instanzen untergeordneter Klassen arbeiten, kann die prototypische Vererbung Ihnen besser dienen. Wie Sie sehen, ist es auch etwas ärgerlich, eine Methode der Basisklasse aufzurufen: Wir müssen uns daran erinnern, was die Methode war, bevor der Unterklassenkonstruktor sie überschrieb, sonst geht sie verloren.
[Auch weil hier keine Vererbung vorhanden ist, instanceof
funktioniert der Operator nicht. Sie müssten Ihren eigenen Mechanismus zum Schnüffeln von Klassen bereitstellen, wenn Sie ihn benötigen. Während Sie die Prototypobjekte auf ähnliche Weise wie bei der Vererbung von Prototypen fummeln könnten , ist es ein bisschen schwierig und es lohnt sich nicht wirklich, es nur zu bekommeninstanceof
Arbeit zu gehen.]
Das Gute an jeder Instanz mit einer eigenen Methode ist, dass die Methode dann an die spezifische Instanz gebunden sein kann, der sie gehört. Dies ist nützlich, weil JavaScript auf seltsame Weise this
in Methodenaufrufen bindet. Dies hat zur Folge, dass, wenn Sie eine Methode von ihrem Eigentümer trennen:
var ts= mycircle.toString;
alert(ts());
dann wird this
innerhalb der Methode nicht wie erwartet die Circle-Instanz sein (es wird tatsächlich das globale window
Objekt sein, was weit verbreitetes Debugging-Leid verursacht). In der Realität geschieht dies normalerweise, wenn eine Methode genommen und einem oder zugewiesen setTimeout
wirdonclick
EventListener
im Allgemeinen.
Bei der Prototyp-Methode müssen Sie für jede solche Aufgabe einen Abschluss hinzufügen:
setTimeout(function() {
mycircle.move(1, 1);
}, 1000);
oder in Zukunft (oder jetzt, wenn Sie Function.prototype hacken) können Sie dies auch tun mit function.bind()
:
setTimeout(mycircle.move.bind(mycircle, 1, 1), 1000);
Wenn Ihre Instanzen auf die Art des Schließens ausgeführt werden, erfolgt die Bindung kostenlos durch das Schließen über die Instanzvariable (normalerweise aufgerufen that
oder self
, obwohl ich persönlich davon abraten würde, da diese self
bereits eine andere, andere Bedeutung in JavaScript hat). Sie erhalten die Argumente 1, 1
im obigen Snippet jedoch nicht kostenlos, sodass Sie immer noch einen weiteren Abschluss benötigen oder einen, bind()
wenn Sie dies tun müssen.
Auch bei der Verschlussmethode gibt es viele Varianten. Sie können es vorziehen, ganz wegzulassen this
, ein neues zu erstellen that
und es zurückzugeben, anstatt den new
Operator zu verwenden:
function Shape(x, y) {
var that= {};
that.x= x;
that.y= y;
that.toString= function() {
return 'Shape at '+that.x+', '+that.y;
};
return that;
}
function Circle(x, y, r) {
var that= Shape(x, y);
that.r= r;
var _baseToString= that.toString;
that.toString= function() {
return 'Circular '+_baseToString(that)+' with radius '+r;
};
return that;
};
var mycircle= Circle(); // you can include `new` if you want but it won't do anything
Welcher Weg ist „richtig“? Beide. Welches das Beste ist"? Das hängt von Ihrer Situation ab. FWIW Ich tendiere zum Prototyping für echte JavaScript-Vererbung, wenn ich stark OO-Sachen mache, und zum Schließen für einfache Wegwerf-Seiteneffekte.
Beide Möglichkeiten sind für die meisten Programmierer jedoch nicht intuitiv. Beide haben viele mögliche unordentliche Variationen. Sie werden beide (sowie viele Zwischen- und allgemein fehlerhafte Schemata) treffen, wenn Sie den Code / die Bibliotheken anderer Personen verwenden. Es gibt keine allgemein akzeptierte Antwort. Willkommen in der wundervollen Welt der JavaScript-Objekte.
[Dies war Teil 94 von Warum JavaScript nicht meine Lieblingsprogrammiersprache ist.]