newEine Klasse und eine Unterklasse
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return `(${this.x}, ${this.y})`;
}
}
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y);
this.color = color;
}
toString() {
return super.toString() + ' in ' + this.color;
}
}
Verwendung der Klassen
> const cp = new ColorPoint(25, 8, 'green');
> cp.toString();
'(25, 8) in green'
> cp instanceof ColorPoint
true
> cp instanceof Point
true
Unter der Haube sind ES6-Klassen nichts radikal Neues: Sie bieten hauptsächlich eine bequemere Syntax zur Erstellung von altmodischen Konstruktorfunktionen. Das kann man sehen, wenn man typeof verwendet.
> typeof Point
'function'
Eine Klasse wird in ECMAScript 6 so definiert
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return `(${this.x}, ${this.y})`;
}
}
Sie verwenden diese Klasse genauso wie eine ES5-Konstruktorfunktion
> var p = new Point(25, 8);
> p.toString()
'(25, 8)'
Tatsächlich ist das Ergebnis einer Klassendefinition eine Funktion
> typeof Point
'function'
Allerdings können Sie eine Klasse nur über new aufrufen, nicht über einen Funktionsaufruf (die Begründung dafür wird später erklärt)
> Point()
TypeError: Classes can’t be function-called
Es gibt keine trennenden Satzzeichen zwischen den Mitgliedern einer Klassendefinition. Beispielsweise sind die Mitglieder eines Objekt-Literals durch Kommas getrennt, die auf der obersten Ebene von Klassendefinitionen illegal sind. Semikolons sind erlaubt, werden aber ignoriert.
class MyClass {
foo() {}
; // OK, ignored
, // SyntaxError
bar() {}
}
Semikolons sind zur Vorbereitung zukünftiger Syntax erlaubt, die Semikolon-terminierte Mitglieder enthalten könnte. Kommas sind verboten, um zu betonen, dass Klassendefinitionen sich von Objekt-Literalen unterscheiden.
Funktionsdeklarationen werden gehoisted: Beim Betreten eines Geltungsbereichs sind die dort deklarierten Funktionen sofort verfügbar – unabhängig davon, wo die Deklarationen stattfinden. Das bedeutet, dass Sie eine Funktion aufrufen können, die später deklariert wird.
foo(); // works, because `foo` is hoisted
function foo() {}
Im Gegensatz dazu werden Klassendeklarationen nicht gehoisted. Daher existiert eine Klasse erst, nachdem die Ausführung ihre Definition erreicht und sie ausgewertet hat. Der Zugriff darauf führt zu einem ReferenceError.
new Foo(); // ReferenceError
class Foo {}
Der Grund für diese Einschränkung ist, dass Klassen eine extends-Klausel haben können, deren Wert ein beliebiger Ausdruck ist. Dieser Ausdruck muss an der richtigen "Stelle" ausgewertet werden, seine Auswertung kann nicht gehoisted werden.
Das Nicht-Hoisting ist weniger einschränkend, als Sie vielleicht denken. Zum Beispiel kann eine Funktion, die vor einer Klassendefinition steht, immer noch auf diese Klasse verweisen, aber Sie müssen warten, bis die Klassendefinition ausgewertet wurde, bevor Sie die Funktion aufrufen können.
function functionThatUsesBar() {
new Bar();
}
functionThatUsesBar(); // ReferenceError
class Bar {}
functionThatUsesBar(); // OK
Ähnlich wie bei Funktionen gibt es zwei Arten von Klassendefinitionen, zwei Möglichkeiten, eine Klasse zu definieren: Klassendeklarationen und Klassenausdrücke.
Ähnlich wie bei Funktionsausdrücken können Klassenausdrücke anonym sein
const MyClass = class {
···
};
const inst = new MyClass();
Ebenso wie bei Funktionsausdrücken können Klassenausdrücke Namen haben, die nur in ihnen sichtbar sind
const MyClass = class Me {
getClassName() {
return Me.name;
}
};
const inst = new MyClass();
console.log(inst.getClassName()); // Me
console.log(Me.name); // ReferenceError: Me is not defined
Die letzten beiden Zeilen zeigen, dass Me keine Variable außerhalb der Klasse wird, aber darin verwendet werden kann.
Ein Klassenkörper kann nur Methoden, aber keine Daten-Eigenschaften enthalten. Prototypen mit Daten-Eigenschaften werden allgemein als Anti-Pattern betrachtet, also erzwingt dies nur eine Best Practice.
constructor, statische Methoden, Prototypenmethoden Betrachten wir drei Arten von Methoden, die Sie oft in Klassendefinitionen finden.
class Foo {
constructor(prop) {
this.prop = prop;
}
static staticMethod() {
return 'classy';
}
prototypeMethod() {
return 'prototypical';
}
}
const foo = new Foo(123);
Das Objektdiagramm für diese Klassendefinition sieht wie folgt aus. Tipp zum Verständnis: [[Prototype]] ist eine Vererbungsbeziehung zwischen Objekten, während prototype eine normale Eigenschaft ist, deren Wert ein Objekt ist. Die Eigenschaft prototype ist nur speziell in Bezug auf den new-Operator, der ihren Wert als Prototyp für erstellte Instanzen verwendet.
Erstens, die Pseudo-Methode constructor. Diese Methode ist besonders, da sie die Funktion definiert, die die Klasse repräsentiert.
> Foo === Foo.prototype.constructor
true
> typeof Foo
'function'
Sie wird manchmal als Klassenkonstruktor bezeichnet. Sie hat Merkmale, die normale Konstruktorfunktionen nicht haben (hauptsächlich die Fähigkeit, ihren Superkonstruktor über super() aufzurufen, was später erklärt wird).
Zweitens, statische Methoden. Statische Eigenschaften (oder Klassen-Eigenschaften) sind Eigenschaften von Foo selbst. Wenn Sie eine Methodendefinition mit static voranstellen, erstellen Sie eine Klassenmethode.
> typeof Foo.staticMethod
'function'
> Foo.staticMethod()
'classy'
Drittens, Prototypenmethoden. Die Prototyp-Eigenschaften von Foo sind die Eigenschaften von Foo.prototype. Sie sind normalerweise Methoden und werden von Instanzen von Foo geerbt.
> typeof Foo.prototype.prototypeMethod
'function'
> foo.prototypeMethod()
'prototypical'
Um ES6-Klassen rechtzeitig fertigzustellen, wurden sie bewusst "maximal minimal" gestaltet. Deshalb können Sie derzeit nur statische Methoden, Getter und Setter erstellen, aber keine statischen Daten-Eigenschaften. Es gibt einen Vorschlag, sie in die Sprache aufzunehmen. Bis dieser Vorschlag angenommen wird, gibt es zwei Workarounds, die Sie verwenden können.
Erstens können Sie manuell eine statische Eigenschaft hinzufügen
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
Point.ZERO = new Point(0, 0);
Sie könnten Object.defineProperty() verwenden, um eine schreibgeschützte Eigenschaft zu erstellen, aber ich mag die Einfachheit einer Zuweisung.
Zweitens können Sie einen statischen Getter erstellen
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
static get ZERO() {
return new Point(0, 0);
}
}
In beiden Fällen erhalten Sie eine Eigenschaft Point.ZERO, die Sie lesen können. Im ersten Fall wird jedes Mal dieselbe Instanz zurückgegeben. Im zweiten Fall wird jedes Mal eine neue Instanz zurückgegeben.
Die Syntax für Getter und Setter ist genau wie in ECMAScript 5 Objekt-Literalen
class MyClass {
get prop() {
return 'getter';
}
set prop(value) {
console.log('setter: '+value);
}
}
Sie verwenden MyClass wie folgt.
> const inst = new MyClass();
> inst.prop = 123;
setter: 123
> inst.prop
'getter'
Sie können den Namen einer Methode über einen Ausdruck definieren, indem Sie ihn in eckige Klammern setzen. Zum Beispiel sind die folgenden Möglichkeiten, Foo zu definieren, alle gleichwertig.
class Foo {
myMethod() {}
}
class Foo {
['my'+'Method']() {}
}
const m = 'myMethod';
class Foo {
[m]() {}
}
Mehrere spezielle Methoden in ECMAScript 6 haben Schlüssel, die Symbole sind. Berechnete Methodennamen erlauben Ihnen, solche Methoden zu definieren. Wenn ein Objekt beispielsweise eine Methode mit dem Schlüssel Symbol.iterator hat, ist es iterierbar. Das bedeutet, dass seine Inhalte mit der for-of-Schleife und anderen Sprachmechanismen durchlaufen werden können.
class IterableClass {
[Symbol.iterator]() {
···
}
}
Wenn Sie eine Methodendefinition mit einem Sternchen (*) voranstellen, wird sie zu einer Generator-Methode. Ein Generator ist unter anderem nützlich, um die Methode zu definieren, deren Schlüssel Symbol.iterator ist. Der folgende Code zeigt, wie das funktioniert.
class IterableArguments {
constructor(...args) {
this.args = args;
}
* [Symbol.iterator]() {
for (const arg of this.args) {
yield arg;
}
}
}
for (const x of new IterableArguments('hello', 'world')) {
console.log(x);
}
// Output:
// hello
// world
Die extends-Klausel ermöglicht es Ihnen, eine Unterklasse einer bestehenden Konstruktorfunktion zu erstellen (die möglicherweise nicht über eine Klasse definiert wurde).
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return `(${this.x}, ${this.y})`;
}
}
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y); // (A)
this.color = color;
}
toString() {
return super.toString() + ' in ' + this.color; // (B)
}
}
Auch hier wird diese Klasse wie erwartet verwendet
> const cp = new ColorPoint(25, 8, 'green');
> cp.toString()
'(25, 8) in green'
> cp instanceof ColorPoint
true
> cp instanceof Point
true
Es gibt zwei Arten von Klassen
Point ist eine Basisklasse, da sie keine extends-Klausel hat.ColorPoint ist eine abgeleitete Klasse.Es gibt zwei Möglichkeiten, super zu verwenden
constructor in einer Klassendefinition) verwendet ihn wie einen Funktionsaufruf (super(···)), um einen Superkonstruktor-Aufruf zu tätigen (Zeile A).static) verwenden ihn wie Eigenschaftsreferenzen (super.prop) oder Methodenaufrufe (super.method(···)), um auf Super-Eigenschaften zu verweisen (Zeile B).Der Prototyp einer Unterklasse ist die Superklasse in ECMAScript 6
> Object.getPrototypeOf(ColorPoint) === Point
true
Das bedeutet, dass statische Eigenschaften vererbt werden
class Foo {
static classMethod() {
return 'hello';
}
}
class Bar extends Foo {
}
Bar.classMethod(); // 'hello'
Sie können sogar statische Methoden per super aufrufen
class Foo {
static classMethod() {
return 'hello';
}
}
class Bar extends Foo {
static classMethod() {
return super.classMethod() + ', too';
}
}
Bar.classMethod(); // 'hello, too'
In einer abgeleiteten Klasse müssen Sie super() aufrufen, bevor Sie this verwenden können.
class Foo {}
class Bar extends Foo {
constructor(num) {
const tmp = num * 2; // OK
this.num = num; // ReferenceError
super();
this.num = num; // OK
}
}
Das implizite Verlassen eines abgeleiteten Konstruktors ohne Aufruf von super() führt ebenfalls zu einem Fehler.
class Foo {}
class Bar extends Foo {
constructor() {
}
}
const bar = new Bar(); // ReferenceError
Genau wie in ES5 können Sie das Ergebnis eines Konstruktors überschreiben, indem Sie explizit ein Objekt zurückgeben.
class Foo {
constructor() {
return Object.create(null);
}
}
console.log(new Foo() instanceof Foo); // false
Wenn Sie dies tun, spielt es keine Rolle, ob this initialisiert wurde oder nicht. Mit anderen Worten: Sie müssen super() in einem abgeleiteten Konstruktor nicht aufrufen, wenn Sie das Ergebnis auf diese Weise überschreiben.
Wenn Sie keinen constructor für eine Basisklasse angeben, wird die folgende Definition verwendet.
constructor() {}
Für abgeleitete Klassen wird der folgende Standardkonstruktor verwendet.
constructor(...args) {
super(...args);
}
In ECMAScript 6 können endlich alle eingebauten Konstruktoren unterklassenbildet werden (es gibt Workarounds für ES5, aber diese haben erhebliche Einschränkungen).
Zum Beispiel können Sie jetzt eigene Ausnahme-Klassen erstellen (die in den meisten Engines die Funktion haben werden, einen Stack-Trace zu besitzen).
class MyError extends Error {
}
throw new MyError('Something happened!');
Sie können auch Unterklassen von Array erstellen, deren Instanzen length korrekt handhaben.
class Stack extends Array {
get top() {
return this[this.length - 1];
}
}
var stack = new Stack();
stack.push('world');
stack.push('hello');
console.log(stack.top); // hello
console.log(stack.length); // 2
Beachten Sie, dass die Unterklassenbildung von Array normalerweise nicht die beste Lösung ist. Es ist oft besser, Ihre eigene Klasse zu erstellen (deren Schnittstelle Sie kontrollieren) und in einer privaten Eigenschaft an ein Array zu delegieren.
Dieser Abschnitt erklärt vier Ansätze zur Verwaltung privater Daten für ES6-Klassen.
constructorAnsätze #1 und #2 waren in ES5 für Konstruktoren bereits üblich. Ansätze #3 und #4 sind neu in ES6. Implementieren wir dasselbe Beispiel viermal, mit jedem der Ansätze.
Unser fortlaufendes Beispiel ist eine Klasse Countdown, die eine Callback-Funktion action aufruft, sobald ein Zähler (dessen Anfangswert counter ist) Null erreicht. Die beiden Parameter action und counter sollen als private Daten gespeichert werden.
In der ersten Implementierung speichern wir action und counter in der Umgebung des Klassenkonstruktors. Eine Umgebung ist die interne Datenstruktur, in der eine JavaScript-Engine die Parameter und lokalen Variablen speichert, die entstehen, wenn ein neuer Geltungsbereich betreten wird (z. B. über einen Funktions- oder Konstruktoraufruf). Dies ist der Code.
class Countdown {
constructor(counter, action) {
Object.assign(this, {
dec() {
if (counter < 1) return;
counter--;
if (counter === 0) {
action();
}
}
});
}
}
Die Verwendung von Countdown sieht wie folgt aus.
> const c = new Countdown(2, () => console.log('DONE'));
> c.dec();
> c.dec();
DONE
Vorteile
Nachteile
Mehr Informationen zu dieser Technik: Abschn. „Private Data in the Environment of a Constructor (Crockford Privacy Pattern)“ in „Speaking JavaScript“.
Der folgende Code speichert private Daten in Eigenschaften, deren Namen durch einen präfixierten Unterstrich markiert sind.
class Countdown {
constructor(counter, action) {
this._counter = counter;
this._action = action;
}
dec() {
if (this._counter < 1) return;
this._counter--;
if (this._counter === 0) {
this._action();
}
}
}
Vorteile
Nachteile
Es gibt eine clevere Technik mit WeakMaps, die den Vorteil des ersten Ansatzes (Sicherheit) mit dem Vorteil des zweiten Ansatzes (Möglichkeit zur Verwendung von Prototypenmethoden) kombiniert. Diese Technik wird im folgenden Code demonstriert: Wir verwenden die WeakMaps _counter und _action, um private Daten zu speichern.
const _counter = new WeakMap();
const _action = new WeakMap();
class Countdown {
constructor(counter, action) {
_counter.set(this, counter);
_action.set(this, action);
}
dec() {
let counter = _counter.get(this);
if (counter < 1) return;
counter--;
_counter.set(this, counter);
if (counter === 0) {
_action.get(this)();
}
}
}
Jede der beiden WeakMaps _counter und _action ordnet Objekte ihren privaten Daten zu. Aufgrund der Funktionsweise von WeakMaps verhindern diese nicht, dass Objekte garbage-collected werden. Solange Sie die WeakMaps vor der Außenwelt verbergen, sind die privaten Daten sicher.
Wenn Sie noch sicherer sein wollen, können Sie WeakMap.prototype.get und WeakMap.prototype.set in Variablen speichern und diese aufrufen (statt der Methoden, dynamisch).
const set = WeakMap.prototype.set;
···
set.call(_counter, this, counter);
// _counter.set(this, counter);
Dann wird Ihr Code nicht beeinträchtigt, wenn bösartiger Code diese Methoden durch solche ersetzt, die unsere privaten Daten ausspionieren. Sie sind jedoch nur vor Code geschützt, der nach Ihrem Code läuft. Sie können nichts tun, wenn er vor Ihrem Code läuft.
Vorteile
Nachteil
Ein weiterer Speicherort für private Daten sind Eigenschaften, deren Schlüssel Symbole sind.
const _counter = Symbol('counter');
const _action = Symbol('action');
class Countdown {
constructor(counter, action) {
this[_counter] = counter;
this[_action] = action;
}
dec() {
if (this[_counter] < 1) return;
this[_counter]--;
if (this[_counter] === 0) {
this[_action]();
}
}
}
Jedes Symbol ist eindeutig, weshalb ein Symbol-wertiger Eigenschaftsschlüssel niemals mit einem anderen Eigenschaftsschlüssel kollidieren wird. Außerdem sind Symbole etwas vor der Außenwelt verborgen, aber nicht vollständig.
const c = new Countdown(2, () => console.log('DONE'));
console.log(Object.keys(c));
// []
console.log(Reflect.ownKeys(c));
// [ Symbol(counter), Symbol(action) ]
Vorteile
Nachteile
Reflect.ownKeys() auflisten.Unterklassenbildung in JavaScript wird aus zwei Gründen verwendet.
instanceof), ist auch eine Instanz der Superklasse. Die Erwartung ist, dass Unterklassen-Instanzen sich wie Superklassen-Instanzen verhalten, aber mehr tun können.Die Nützlichkeit von Klassen für die Implementierungsvererbung ist begrenzt, da sie nur einfache Vererbung unterstützen (eine Klasse kann höchstens eine Superklasse haben). Daher ist es unmöglich, Tool-Methoden aus mehreren Quellen zu erben – sie müssen alle von der Superklasse stammen.
Wie können wir dieses Problem lösen? Betrachten wir eine Lösung anhand eines Beispiels. Betrachten Sie ein Managementsystem für ein Unternehmen, bei dem Employee eine Unterklasse von Person ist.
class Person { ··· }
class Employee extends Person { ··· }
Zusätzlich gibt es Tool-Klassen für die Speicherung und für die Datenvalidierung.
class Storage {
save(database) { ··· }
}
class Validation {
validate(schema) { ··· }
}
Es wäre schön, wenn wir die Tool-Klassen so einbinden könnten.
// Invented ES6 syntax:
class Employee extends Storage, Validation, Person { ··· }
Das heißt, wir wollen, dass Employee eine Unterklasse von Storage ist, die eine Unterklasse von Validation sein sollte, die eine Unterklasse von Person sein sollte. Employee und Person werden nur in einer solchen Klassenkette verwendet. Aber Storage und Validation werden mehrfach verwendet. Wir wollen, dass sie Vorlagen für Klassen sind, deren Superklassen wir ausfüllen. Solche Vorlagen werden abstrakte Unterklassen oder Mixins genannt.
Eine Möglichkeit, ein Mixin in ES6 zu implementieren, besteht darin, es als eine Funktion zu betrachten, deren Eingabe eine Superklasse und deren Ausgabe eine Unterklasse ist, die diese Superklasse erweitert.
const Storage = Sup => class extends Sup {
save(database) { ··· }
};
const Validation = Sup => class extends Sup {
validate(schema) { ··· }
};
Hier profitieren wir davon, dass der Operand der extends-Klausel kein fester Bezeichner ist, sondern ein beliebiger Ausdruck. Mit diesen Mixins wird Employee wie folgt erstellt.
class Employee extends Storage(Validation(Person)) { ··· }
Anerkennung. Das erste Vorkommen dieser Technik, das mir bekannt ist, ist ein Gist von Sebastian Markbåge.
Was wir bisher gesehen haben, sind die Grundlagen von Klassen. Sie müssen nur weiterlesen, wenn Sie daran interessiert sind, wie die Dinge unter der Haube ablaufen. Beginnen wir mit der Syntax von Klassen. Das Folgende ist eine leicht modifizierte Version der Syntax, die in Abschnitt A.4 der ECMAScript 6-Spezifikation gezeigt wird.
ClassDeclaration:
"class" BindingIdentifier ClassTail
ClassExpression:
"class" BindingIdentifier? ClassTail
ClassTail:
ClassHeritage? "{" ClassBody? "}"
ClassHeritage:
"extends" AssignmentExpression
ClassBody:
ClassElement+
ClassElement:
MethodDefinition
"static" MethodDefinition
";"
MethodDefinition:
PropName "(" FormalParams ")" "{" FuncBody "}"
"*" PropName "(" FormalParams ")" "{" GeneratorBody "}"
"get" PropName "(" ")" "{" FuncBody "}"
"set" PropName "(" PropSetParams ")" "{" FuncBody "}"
PropertyName:
LiteralPropertyName
ComputedPropertyName
LiteralPropertyName:
IdentifierName /* foo */
StringLiteral /* "foo" */
NumericLiteral /* 123.45, 0xFF */
ComputedPropertyName:
"[" Expression "]"
Zwei Beobachtungen
class Foo extends combine(MyMixin, MySuperClass) {}
eval oder arguments sein; doppelte Klassenelementnamen sind nicht erlaubt; der Name constructor darf nur für eine normale Methode verwendet werden, nicht für einen Getter, einen Setter oder eine Generator-Methode.TypeException, wenn sie aufgerufen werden. class C {
m() {}
}
new C.prototype.m(); // TypeError
Klassendefinitionen erstellen (mutable) let-Bindungen. Die folgende Tabelle beschreibt die Attribute von Eigenschaften, die sich auf eine gegebene Klasse Foo beziehen.
| schreibbar | aufzählbar | konfigurierbar | |
|---|---|---|---|
Statische Eigenschaften Foo.* |
true |
false |
true |
Foo.prototype |
false |
false |
false |
Foo.prototype.constructor |
false |
false |
true |
Prototyp-Eigenschaften Foo.prototype.* |
true |
false |
true |
Hinweise
prototype haben eine unveränderliche bidirektionale Verbindung.Klassen haben lexikalische interne Namen, genau wie benannte Funktionsausdrücke.
Sie wissen vielleicht, dass benannte Funktionsausdrücke lexikalische interne Namen haben.
const fac = function me(n) {
if (n > 0) {
// Use inner name `me` to
// refer to function
return n * me(n-1);
} else {
return 1;
}
};
console.log(fac(3)); // 6
Der Name me des benannten Funktionsausdrucks wird zu einer lexikalisch gebundenen Variablen, die unabhängig davon ist, welche Variable derzeit die Funktion enthält.
Interessanterweise haben ES6-Klassen auch lexikalische interne Namen, die Sie in Methoden (Konstruktor-Methoden und reguläre Methoden) verwenden können.
class C {
constructor() {
// Use inner name C to refer to class
console.log(`constructor: ${C.prop}`);
}
logProp() {
// Use inner name C to refer to class
console.log(`logProp: ${C.prop}`);
}
}
C.prop = 'Hi!';
const D = C;
C = null;
// C is not a class, anymore:
new C().logProp();
// TypeError: C is not a function
// But inside the class, the identifier C
// still works
new D().logProp();
// constructor: Hi!
// logProp: Hi!
(In der ES6-Spezifikation wird der interne Name durch die dynamische Semantik von ClassDefinitionEvaluation eingerichtet.)
Anerkennung: Vielen Dank an Michael Ficarra für den Hinweis, dass Klassen interne Namen haben.
In ECMAScript 6 sieht die Unterklassenbildung wie folgt aus.
class Person {
constructor(name) {
this.name = name;
}
toString() {
return `Person named ${this.name}`;
}
static logNames(persons) {
for (const person of persons) {
console.log(person.name);
}
}
}
class Employee extends Person {
constructor(name, title) {
super(name);
this.title = title;
}
toString() {
return `${super.toString()} (${this.title})`;
}
}
const jane = new Employee('Jane', 'CTO');
console.log(jane.toString()); // Person named Jane (CTO)
Der nächste Abschnitt untersucht die Struktur der Objekte, die im vorherigen Beispiel erstellt wurden. Der darauf folgende Abschnitt untersucht, wie jane zugewiesen und initialisiert wird.
Das vorherige Beispiel erstellt die folgenden Objekte.
Prototypketten sind Objekte, die über die [[Prototype]]-Beziehung (eine Vererbungsbeziehung) verknüpft sind. Im Diagramm sehen Sie zwei Prototypketten.
Der Prototyp einer abgeleiteten Klasse ist die Klasse, die sie erweitert. Der Grund für diese Einrichtung ist, dass Sie möchten, dass eine Unterklasse alle Eigenschaften ihrer Superklasse erbt.
> Employee.logNames === Person.logNames
true
Der Prototyp einer Basisklasse ist Function.prototype, was auch der Prototyp von Funktionen ist.
> const getProto = Object.getPrototypeOf.bind(Object);
> getProto(Person) === Function.prototype
true
> getProto(function () {}) === Function.prototype
true
Das bedeutet, dass Basisklassen und alle ihre abgeleiteten Klassen (ihre Prototypen) Funktionen sind. Traditionelle ES5-Funktionen sind im Wesentlichen Basisklassen.
Der Hauptzweck einer Klasse ist die Einrichtung dieser Prototypkette. Die Prototypkette endet mit Object.prototype (dessen Prototyp null ist). Das macht Object zu einer impliziten Superklasse jeder Basisklasse (soweit Instanzen und der instanceof-Operator betroffen sind).
Der Grund für diese Einrichtung ist, dass Sie möchten, dass der Instanz-Prototyp einer Unterklasse alle Eigenschaften des Superklassen-Instanz-Prototyps erbt.
Nebenbei bemerkt, haben Objekte, die über Objekt-Literale erstellt wurden, ebenfalls den Prototyp Object.prototype.
> Object.getPrototypeOf({}) === Object.prototype
true
Der Datenfluss zwischen Klassenkonstruktoren unterscheidet sich von der kanonischen Art der Unterklassenbildung in ES5. Unter der Haube sieht er grob wie folgt aus.
// Base class: this is where the instance is allocated
function Person(name) {
// Performed before entering this constructor:
this = Object.create(new.target.prototype);
this.name = name;
}
···
function Employee(name, title) {
// Performed before entering this constructor:
this = uninitialized;
this = Reflect.construct(Person, [name], new.target); // (A)
// super(name);
this.title = title;
}
Object.setPrototypeOf(Employee, Person);
···
const jane = Reflect.construct( // (B)
Employee, ['Jane', 'CTO'],
Employee);
// const jane = new Employee('Jane', 'CTO')
Das Instanzobjekt wird in ES6 und ES5 an unterschiedlichen Stellen erstellt.
super() aufgerufen, was einen Konstruktoraufruf auslöst.new erstellt, dem ersten in einer Kette von Konstruktoraufrufen. Der Superkonstruktor wird über einen Funktionsaufruf aufgerufen.Der vorherige Code verwendet zwei neue ES6-Funktionen.
new.target ist ein impliziter Parameter, den alle Funktionen haben. In einer Kette von Konstruktoraufrufen ist seine Rolle ähnlich wie die von this in einer Kette von Supermethodenaufrufen.new aufgerufen wird (wie in Zeile B), ist der Wert von new.target dieser Konstruktor.super() aufgerufen wird (wie in Zeile A), ist der Wert von new.target der new.target des Konstruktors, der den Aufruf tätigt.undefined. Das bedeutet, dass Sie new.target verwenden können, um festzustellen, ob eine Funktion als Funktion aufgerufen oder als Konstruktor (über new) aufgerufen wurde.new.target auf den new.target der umgebenden Nicht-Pfeilfunktion.Reflect.construct() ermöglicht es Ihnen, Konstruktoraufrufe zu tätigen und dabei new.target über den letzten Parameter anzugeben.Der Vorteil dieser Art der Unterklassenbildung besteht darin, dass sie es normalem Code ermöglicht, eingebaute Konstruktoren (wie Error und Array) zu unterklassenbilden. Ein späterer Abschnitt erklärt, warum ein anderer Ansatz notwendig war.
Zur Erinnerung, hier ist, wie man in ES5 eine Unterklasse bildet.
function Person(name) {
this.name = name;
}
···
function Employee(name, title) {
Person.call(this, name);
this.title = title;
}
Employee.prototype = Object.create(Person.prototype);
Employee.prototype.constructor = Employee;
···
this ist ursprünglich in abgeleiteten Konstruktoren nicht initialisiert, was bedeutet, dass ein Fehler ausgelöst wird, wenn sie this auf irgendeine Weise verwenden, bevor sie super() aufgerufen haben.this initialisiert ist, erzeugt der Aufruf von super() einen ReferenceError. Dies schützt Sie davor, super() zweimal aufzurufen.return-Anweisung), ist das Ergebnis this. Wenn this nicht initialisiert ist, wird ein ReferenceError ausgelöst. Dies schützt Sie davor, den Aufruf von super() zu vergessen.undefined und null), ist das Ergebnis this (dieses Verhalten ist erforderlich, um mit ES5 und früher kompatibel zu bleiben). Wenn this nicht initialisiert ist, wird ein TypeError ausgelöst.this initialisiert ist oder nicht.extends-Klausel Betrachten wir, wie die extends-Klausel beeinflusst, wie eine Klasse eingerichtet wird (Abschnitt 14.5.14 der Spezifikation).
Der Wert einer extends-Klausel muss "konstruierbar" sein (aufrufbar über new). null ist jedoch erlaubt.
class C {
}
C: Function.prototype (wie eine normale Funktion)C.prototype: Object.prototype (was auch der Prototyp von über Objekt-Literale erstellten Objekten ist)class C extends B {
}
C: BC.prototype: B.prototypeclass C extends Object {
}
C: ObjectC.prototype: Object.prototypeBeachten Sie den folgenden subtilen Unterschied zum ersten Fall: Wenn keine extends-Klausel vorhanden ist, ist die Klasse eine Basisklasse und weist Instanzen zu. Wenn eine Klasse Object erweitert, ist sie eine abgeleitete Klasse und Object weist die Instanzen zu. Die resultierenden Instanzen (einschließlich ihrer Prototypketten) sind dieselben, aber Sie gelangen auf unterschiedlichem Weg dorthin.
class C extends null {
}
C: Function.prototypeC.prototype: nullEine solche Klasse ermöglicht es Ihnen, Object.prototype in der Prototypkette zu vermeiden.
In ECMAScript 5 können die meisten eingebauten Konstruktoren nicht unterklassenbildet werden (es existieren verschiedene Workarounds).
Um das zu verstehen, verwenden wir das kanonische ES5-Muster, um Array unterklassenzubilden. Wie wir bald feststellen werden, funktioniert das nicht.
function MyArray(len) {
Array.call(this, len); // (A)
}
MyArray.prototype = Object.create(Array.prototype);
Leider stellen wir bei der Instanziierung von MyArray fest, dass es nicht richtig funktioniert: Die Instanz-Eigenschaft length ändert sich nicht als Reaktion auf das Hinzufügen von Array-Elementen.
> var myArr = new MyArray(0);
> myArr.length
0
> myArr[0] = 'foo';
> myArr.length
0
Zwei Hindernisse verhindern, dass myArr ein richtiges Array ist.
Erstes Hindernis: Initialisierung. Das this, das Sie dem Konstruktor Array übergeben (in Zeile A), wird komplett ignoriert. Das bedeutet, Sie können Array nicht verwenden, um die für MyArray erstellte Instanz einzurichten.
> var a = [];
> var b = Array.call(a, 3);
> a !== b // a is ignored, b is a new object
true
> b.length // set up correctly
3
> a.length // unchanged
0
Zweites Hindernis: Zuweisung. Die von Array erstellten Instanzobjekte sind exotisch (ein Begriff, der von der ECMAScript-Spezifikation für Objekte verwendet wird, die über normale Objekte hinausgehende Merkmale aufweisen): Ihre Eigenschaft length verfolgt und beeinflusst die Verwaltung von Array-Elementen. Im Allgemeinen können exotische Objekte von Grund auf neu erstellt werden, aber Sie können kein bestehendes normales Objekt in ein exotisches umwandeln. Leider müsste Array dies tun, wenn es in Zeile A aufgerufen würde: Es müsste das für MyArray erstellte normale Objekt in ein exotisches Array-Objekt umwandeln.
In ECMAScript 6 sieht die Unterklassenbildung von Array wie folgt aus.
class MyArray extends Array {
constructor(len) {
super(len);
}
}
Das funktioniert.
> const myArr = new MyArray(0);
> myArr.length
0
> myArr[0] = 'foo';
> myArr.length
1
Betrachten wir, wie der ES6-Ansatz zur Unterklassenbildung die zuvor genannten Hindernisse beseitigt.
Array keine Instanz einrichten kann, wird dadurch beseitigt, dass Array eine vollständig konfigurierte Instanz zurückgibt. Im Gegensatz zu ES5 hat diese Instanz den Prototyp der Unterklasse.Der folgende ES6-Code macht in Zeile B einen Super-Methodenaufruf.
class Person {
constructor(name) {
this.name = name;
}
toString() { // (A)
return `Person named ${this.name}`;
}
}
class Employee extends Person {
constructor(name, title) {
super(name);
this.title = title;
}
toString() {
return `${super.toString()} (${this.title})`; // (B)
}
}
const jane = new Employee('Jane', 'CTO');
console.log(jane.toString()); // Person named Jane (CTO)
Um zu verstehen, wie Super-Aufrufe funktionieren, sehen wir uns das Objekt-Diagramm von jane an.
In Zeile B macht Employee.prototype.toString einen Super-Aufruf (Zeile B) zur Methode (beginnend in Zeile A), die sie überschrieben hat. Nennen wir das Objekt, in dem eine Methode gespeichert ist, das Home-Objekt dieser Methode. Employee.prototype ist zum Beispiel das Home-Objekt von Employee.prototype.toString().
Der Super-Aufruf in Zeile B umfasst drei Schritte:
toString. Diese Methode kann in dem Objekt gefunden werden, in dem die Suche begann, oder später in der Prototypenkette.this auf. Der Grund dafür ist: Die per Super-Aufruf aufgerufene Methode muss auf dieselben Instanzeigenschaften zugreifen können (in unserem Beispiel die eigenen Eigenschaften von jane).Beachten Sie, dass this auch beim bloßen Abrufen (super.prop) oder Setzen (super.prop = 123) einer Super-Eigenschaft (im Gegensatz zu einem Methodenaufruf) (intern) eine Rolle in Schritt 3 spielen kann, da ein Getter oder Setter aufgerufen werden könnte.
Lassen Sie uns diese Schritte auf drei verschiedene – aber äquivalente – Arten ausdrücken:
// Variation 1: supermethod calls in ES5
var result = Person.prototype.toString.call(this) // steps 1,2,3
// Variation 2: ES5, refactored
var superObject = Person.prototype; // step 1
var superMethod = superObject.toString; // step 2
var result = superMethod.call(this) // step 3
// Variation 3: ES6
var homeObject = Employee.prototype;
var superObject = Object.getPrototypeOf(homeObject); // step 1
var superMethod = superObject.toString; // step 2
var result = superMethod.call(this) // step 3
Variante 3 ist, wie ECMAScript 6 Super-Aufrufe handhabt. Dieser Ansatz wird von zwei internen Bindings unterstützt, die die Umgebungen von Funktionen haben (Umgebungen bieten Speicherplatz, sogenannte Bindings, für die Variablen in einem Scope).
[[thisValue]]: Dieses interne Binding existiert auch in ECMAScript 5 und speichert den Wert von this.[[HomeObject]]: Verweist auf das Home-Objekt der Funktion der Umgebung. Gefüllt über den internen Slot [[HomeObject]], den alle Methoden haben, die super verwenden. Sowohl das Binding als auch der Slot sind neu in ECMAScript 6.super verwenden? Das Verweisen auf Super-Eigenschaften ist praktisch, wenn Prototypenkette beteiligt sind, weshalb Sie es in Methoden-Definitionen (einschließlich Generator-Methoden-Definitionen, Getter und Setter) in Objekt-Literalen und Klassendefinitionen verwenden können. Die Klasse kann abgeleitet sein oder nicht, die Methode kann statisch sein oder nicht.
Die Verwendung von super zum Verweisen auf eine Eigenschaft ist in Funktionsdeklarationen, Funktionsausdrücken und Generatorfunktionen nicht erlaubt.
super verwendet, kann nicht verschoben werden Sie können eine Methode, die super verwendet, nicht verschieben: Eine solche Methode hat den internen Slot [[HomeObject]], der sie an das Objekt bindet, in dem sie erstellt wurde. Wenn Sie sie durch Zuweisung verschieben, wird sie weiterhin auf die Super-Eigenschaften des ursprünglichen Objekts verweisen. In zukünftigen ECMAScript-Versionen kann es möglicherweise eine Möglichkeit geben, auch solche Methoden zu übertragen.
Ein weiterer Mechanismus von integrierten Konstruktoren wurde in ECMAScript 6 erweiterbar gemacht: Manchmal erstellt eine Methode neue Instanzen ihrer Klasse. Wenn Sie eine Unterklasse erstellen – sollte die Methode eine Instanz ihrer Klasse oder eine Instanz der Unterklasse zurückgeben? Einige integrierte ES6-Methoden ermöglichen es Ihnen, zu konfigurieren, wie sie Instanzen über das sogenannte Species-Pattern erstellen.
Betrachten Sie als Beispiel eine Unterklasse SortedArray von Array. Wenn wir map() auf Instanzen dieser Klasse aufrufen, möchten wir, dass sie Instanzen von Array zurückgibt, um unnötiges Sortieren zu vermeiden. Standardmäßig gibt map() Instanzen des Empfängers (this) zurück, aber das Species-Pattern ermöglicht es Ihnen, dies zu ändern.
In den folgenden drei Abschnitten verwende ich zwei Hilfsfunktionen in den Beispielen.
function isObject(value) {
return (value !== null
&& (typeof value === 'object'
|| typeof value === 'function'));
}
/**
* Spec-internal operation that determines whether `x`
* can be used as a constructor.
*/
function isConstructor(x) {
···
}
Das Standard-Species-Pattern wird von Promise.prototype.then(), der filter()-Methode von Typed Arrays und anderen Operationen verwendet. Es funktioniert wie folgt:
this.constructor[Symbol.species] existiert, verwenden Sie es als Konstruktor für die neue Instanz.Array für Arrays).In JavaScript implementiert, würde das Muster wie folgt aussehen:
function SpeciesConstructor(O, defaultConstructor) {
const C = O.constructor;
if (C === undefined) {
return defaultConstructor;
}
if (! isObject(C)) {
throw new TypeError();
}
const S = C[Symbol.species];
if (S === undefined || S === null) {
return defaultConstructor;
}
if (! isConstructor(S)) {
throw new TypeError();
}
return S;
}
Normale Arrays implementieren das Species-Pattern geringfügig anders:
function ArraySpeciesCreate(self, length) {
let C = undefined;
// If the receiver `self` is an Array,
// we use the species pattern
if (Array.isArray(self)) {
C = self.constructor;
if (isObject(C)) {
C = C[Symbol.species];
}
}
// Either `self` is not an Array or the species
// pattern didn’t work out:
// create and return an Array
if (C === undefined || C === null) {
return new Array(length);
}
if (! IsConstructor(C)) {
throw new TypeError();
}
return new C(length);
}
Array.prototype.map() erstellt das zurückgegebene Array über ArraySpeciesCreate(this, this.length).
Promises verwenden eine Variante des Species-Patterns für statische Methoden wie Promise.all().
let C = this; // default
if (! isObject(C)) {
throw new TypeError();
}
// The default can be overridden via the property `C[Symbol.species]`
const S = C[Symbol.species];
if (S !== undefined && S !== null) {
C = S;
}
if (!IsConstructor(C)) {
throw new TypeError();
}
const instance = new C(···);
Dies ist der Standard-Getter für die Eigenschaft [Symbol.species].
static get [Symbol.species]() {
return this;
}
Dieser Standard-Getter wird von den integrierten Klassen Array, ArrayBuffer, Map, Promise, RegExp, Set und %TypedArray% implementiert. Er wird automatisch von Unterklassen dieser integrierten Klassen vererbt.
Es gibt zwei Möglichkeiten, den Standard-Species zu überschreiben: mit einem Konstruktor Ihrer Wahl oder mit null.
Sie können den Standard-Species über einen statischen Getter (Zeile A) überschreiben.
class MyArray1 extends Array {
static get [Symbol.species]() { // (A)
return Array;
}
}
Dadurch gibt map() eine Instanz von Array zurück.
const result1 = new MyArray1().map(x => x);
console.log(result1 instanceof Array); // true
Wenn Sie den Standard-Species nicht überschreiben, gibt map() eine Instanz der Unterklasse zurück.
class MyArray2 extends Array { }
const result2 = new MyArray2().map(x => x);
console.log(result2 instanceof MyArray2); // true
Wenn Sie keinen statischen Getter verwenden möchten, müssen Sie Object.defineProperty() verwenden. Sie können keine Zuweisung verwenden, da bereits eine Eigenschaft mit diesem Schlüssel existiert, die nur einen Getter hat. Das bedeutet, dass sie schreibgeschützt ist und nicht zugewiesen werden kann.
Zum Beispiel setzen wir hier den Species von MyArray1 auf Array.
Object.defineProperty(
MyArray1, Symbol.species, {
value: Array
});
null Wenn Sie den Species auf null setzen, wird der Standardkonstruktor verwendet (welcher das ist, hängt davon ab, welche Variante des Species-Patterns verwendet wird, konsultieren Sie die vorherigen Abschnitte für weitere Informationen).
class MyArray3 extends Array {
static get [Symbol.species]() {
return null;
}
}
const result3 = new MyArray3().map(x => x);
console.log(result3 instanceof Array); // true
Klassen sind in der JavaScript-Community umstritten: Einerseits freuen sich Leute aus klassenbasierten Sprachen, dass sie sich nicht mehr mit den unkonventionellen Vererbungsmechanismen von JavaScript auseinandersetzen müssen. Andererseits argumentieren viele JavaScript-Programmierer, dass das Komplizierte an JavaScript nicht die prototypische Vererbung ist, sondern Konstruktoren.
ES6-Klassen bieten einige klare Vorteile:
Werfen wir einen Blick auf einige häufige Beschwerden über ES6-Klassen. Sie werden sehen, dass ich den meisten zustimme, aber ich denke auch, dass die Vorteile von Klassen ihre Nachteile bei weitem überwiegen. Ich bin froh, dass sie in ES6 enthalten sind und empfehle, sie zu verwenden.
Ja, ES6-Klassen verschleiern die wahre Natur der JavaScript-Vererbung. Es gibt eine unglückliche Trennung zwischen dem Aussehen einer Klasse (ihrer Syntax) und ihrem Verhalten (ihrer Semantik): Sie sieht aus wie ein Objekt, ist aber eine Funktion. Meine Präferenz wäre, dass Klassen Konstruktorobjekte und nicht Konstruktorfunktionen wären. Ich untersuche diesen Ansatz im Projekt Proto.js, über eine kleine Bibliothek (was beweist, wie gut dieser Ansatz passt).
Allerdings ist Abwärtskompatibilität wichtig, weshalb Klassen auch als Konstruktorfunktionen sinnvoll sind. Auf diese Weise sind ES6-Code und ES5 besser interoperabel.
Die Trennung zwischen Syntax und Semantik wird in ES6 und späteren Versionen zu einigen Reibungen führen. Aber Sie können ein angenehmes Leben führen, indem Sie ES6-Klassen einfach beim Wort nehmen. Ich glaube nicht, dass die Illusion Sie jemals beißen wird. Neulinge können schneller einsteigen und später nachlesen, was hinter den Kulissen vor sich geht (nachdem sie sich mit der Sprache wohler fühlen).
Klassen bieten nur einfache Vererbung, was Ihre Ausdrucksfreiheit in Bezug auf objektorientiertes Design stark einschränkt. Der Plan war jedoch schon immer, dass sie die Grundlage für einen Mechanismus der Mehrfachvererbung wie Traits bilden.
Dann wird eine Klasse zu einer instanziierbaren Einheit und einem Ort, an dem Sie Traits zusammenstellen. Bis dahin müssen Sie auf Bibliotheken zurückgreifen, wenn Sie Mehrfachvererbung wünschen.
new ein Wenn Sie eine Klasse instanziieren möchten, sind Sie in ES6 gezwungen, new zu verwenden. Das bedeutet, dass Sie nicht von einer Klasse zu einer Factory-Funktion wechseln können, ohne die Aufrufstellen zu ändern. Das ist in der Tat eine Einschränkung, aber es gibt zwei mildernde Faktoren:
new-Operator zurückgegebene Standardergebnis überschreiben, indem Sie ein Objekt aus der constructor-Methode einer Klasse zurückgeben.new zu einem Funktionsaufruf einfach sein. Offensichtlich hilft das nicht, wenn Sie den Code, der Ihren Code aufruft, nicht kontrollieren, wie es bei Bibliotheken der Fall ist.Daher schränken Klassen Sie syntaktisch etwas ein, aber sobald JavaScript Traits hat, werden sie Sie konzeptionell (in Bezug auf objektorientiertes Design) nicht mehr einschränken.
Das Aufrufen von Klassen als Funktion ist derzeit verboten. Dies geschah, um Optionen für die Zukunft offen zu halten, um schließlich eine Möglichkeit zur Behandlung von Funktionsaufrufen über Klassen hinzuzufügen.
Was ist das Analogon von Function.prototype.apply() für Klassen? Das heißt, wenn ich eine Klasse TheClass und ein Array args von Argumenten habe, wie instanziiere ich TheClass?
Eine Möglichkeit, dies zu tun, ist über den Spread-Operator (...).
function instantiate(TheClass, args) {
return new TheClass(...args);
}
Eine weitere Option ist die Verwendung von Reflect.construct().
function instantiate(TheClass, args) {
return Reflect.construct(TheClass, args);
}
Das Designmotto für Klassen war "maximal minimalistisch". Mehrere fortgeschrittene Features wurden diskutiert, aber letztendlich verworfen, um ein Design zu erhalten, das von TC39 einstimmig akzeptiert würde.
Kommende Versionen von ECMAScript können dieses minimale Design nun erweitern – Klassen bieten eine Grundlage für Features wie Traits (oder Mixins), Value-Objekte (bei denen verschiedene Objekte gleich sind, wenn sie denselben Inhalt haben) und Const-Klassen (die unveränderliche Instanzen erzeugen).
Das folgende Dokument ist eine wichtige Quelle für dieses Kapitel: