.__proto__ vs. .prototypePerson.prototype.constructor (fortgeschritten)this zum Zugriff auf statische private FelderObject.prototype (fortgeschritten)Object.prototype-Methoden auf sichere WeiseObject.prototype.toString()Object.prototype.toLocaleString()Object.prototype.valueOf()Object.prototype.isPrototypeOf()Object.prototype.propertyIsEnumerable()Object.prototype.__proto__ (Accessor)Object.prototype.hasOwnProperty()In diesem Buch wird der objektorientierte Programmierstil (OOP) von JavaScript in vier Schritten eingeführt. Dieses Kapitel behandelt die Schritte 3 und 4, das vorherige Kapitel behandelt die Schritte 1 und 2. Die Schritte sind (Abb. 12)
Oberklasse
class Person {
#firstName; // (A)
constructor(firstName) {
this.#firstName = firstName; // (B)
}
describe() {
return `Person named ${this.#firstName}`;
}
static extractNames(persons) {
return persons.map(person => person.#firstName);
}
}
const tarzan = new Person('Tarzan');
assert.equal(
tarzan.describe(),
'Person named Tarzan'
);
assert.deepEqual(
Person.extractNames([tarzan, new Person('Cheeta')]),
['Tarzan', 'Cheeta']
);Unterklasse
class Employee extends Person {
constructor(firstName, title) {
super(firstName);
this.title = title; // (C)
}
describe() {
return super.describe() +
` (${this.title})`;
}
}
const jane = new Employee('Jane', 'CTO');
assert.equal(
jane.title,
'CTO'
);
assert.equal(
jane.describe(),
'Person named Jane (CTO)'
);Hinweise
.#firstName ist ein privates Feld und muss deklariert werden (Zeile A), bevor es initialisiert werden kann (Zeile B)..title ist eine Eigenschaft und kann ohne vorherige Deklaration initialisiert werden (Zeile C). JavaScript macht Instanzdaten relativ oft öffentlich (im Gegensatz zu z. B. Java, das sie lieber versteckt).Klassen sind im Grunde eine kompakte Syntax zum Einrichten von Prototypketten (die im vorherigen Kapitel erklärt werden). Unter der Haube sind die Klassen von JavaScript unkonventionell. Aber das ist etwas, das wir bei der Arbeit mit ihnen selten sehen. Sie sollten sich normalerweise für Leute, die andere objektorientierte Programmiersprachen verwendet haben, vertraut anfühlen.
Beachten Sie, dass wir keine Klassen benötigen, um Objekte zu erstellen. Wir können dies auch über Objektliterale tun. Deshalb ist das Singleton-Muster in JavaScript nicht notwendig und Klassen werden weniger verwendet als in vielen anderen Sprachen, die sie haben.
Wir haben zuvor mit jane und tarzan gearbeitet, einzelnen Objekten, die Personen repräsentieren. Verwenden wir eine Klassendeklaration, um eine Fabrik für solche Objekte zu implementieren
class Person {
#firstName; // (A)
constructor(firstName) {
this.#firstName = firstName; // (B)
}
describe() {
return `Person named ${this.#firstName}`;
}
static extractNames(persons) {
return persons.map(person => person.#firstName);
}
}jane und tarzan können jetzt über new Person() erstellt werden
const jane = new Person('Jane');
const tarzan = new Person('Tarzan');Untersuchen wir, was sich im Körper der Klasse Person befindet.
.constructor() ist eine spezielle Methode, die nach der Erstellung einer neuen Instanz aufgerufen wird. Darin bezieht sich this auf diese Instanz.
[ES2022] .#firstName ist ein Instanz-privates Feld: Solche Felder werden in Instanzen gespeichert. Sie werden ähnlich wie Eigenschaften aufgerufen, aber ihre Namen sind getrennt – sie beginnen immer mit Hashtags (#). Und sie sind für die Welt außerhalb der Klasse unsichtbar
assert.deepEqual(
Reflect.ownKeys(jane),
[]
);Bevor wir .#firstName im Konstruktor initialisieren können (Zeile B), müssen wir es deklarieren, indem wir es im Klassenkörper erwähnen (Zeile A).
.describe() ist eine Methode. Wenn wir sie über obj.describe() aufrufen, bezieht sich this im Körper von .describe() auf obj.
assert.equal(
jane.describe(), 'Person named Jane'
);
assert.equal(
tarzan.describe(), 'Person named Tarzan'
);.extractName() ist eine statische Methode. „Statisch“ bedeutet, dass sie zur Klasse gehört, nicht zu Instanzen
assert.deepEqual(
Person.extractNames([jane, tarzan]),
['Jane', 'Tarzan']
);Wir können auch Instanzeigenschaften (öffentliche Felder) in Konstruktoren erstellen
class Container {
constructor(value) {
this.value = value;
}
}
const abcContainer = new Container('abc');
assert.equal(
abcContainer.value, 'abc'
);Im Gegensatz zu Instanz-privaten Feldern müssen Instanzeigenschaften nicht in Klassenkörpern deklariert werden.
Es gibt zwei Arten von Klassendefinitionen (Arten der Klassendefinition)
Klassenausdrücke können anonym und benannt sein
// Anonymous class expression
const Person = class { ··· };
// Named class expression
const Person = class MyClass { ··· };Der Name eines benannten Klassenausdrucks funktioniert ähnlich wie der Name eines benannten Funktionsausdrucks: Er kann nur innerhalb des Körpers einer Klasse aufgerufen werden und bleibt derselbe, unabhängig davon, dem die Klasse zugewiesen wird.
instanceof-OperatorDer instanceof-Operator sagt uns, ob ein Wert eine Instanz einer gegebenen Klasse ist
> new Person('Jane') instanceof Person
true
> {} instanceof Person
false
> {} instanceof Object
true
> [] instanceof Array
trueWir werden den instanceof-Operator später detaillierter untersuchen, nachdem wir uns mit der Unterklassenbildung beschäftigt haben.
In der Programmiersprache JavaScript können Objekte zwei Arten von „Slots“ haben.
Das sind die wichtigsten Regeln, die wir über Eigenschaften und private Slots wissen müssen
static verwendet wird und von anderen Faktoren. Mehr Informationen zu Eigenschaften und privaten Slots
Dieses Kapitel behandelt nicht alle Details zu Eigenschaften und privaten Slots (nur das Wesentliche). Wenn Sie tiefer graben möchten, können Sie dies hier tun
[[PrivateElements]]“.Die folgende Klasse demonstriert die beiden Arten von Slots. Jede ihrer Instanzen hat ein privates Feld und eine Eigenschaft
class MyClass {
#instancePrivateField = 1;
instanceProperty = 2;
getInstanceValues() {
return [
this.#instancePrivateField,
this.instanceProperty,
];
}
}
const inst = new MyClass();
assert.deepEqual(
inst.getInstanceValues(), [1, 2]
);Wie erwartet, außerhalb von MyClass können wir nur die Eigenschaft sehen
assert.deepEqual(
Reflect.ownKeys(inst),
['instanceProperty']
);Als Nächstes sehen wir uns einige Details zu privaten Slots an.
Ein privater Slot kann wirklich nur im Körper seiner Klasse aufgerufen werden. Wir können ihn nicht einmal von einer Unterklasse aus aufrufen
class SuperClass {
#superProp = 'superProp';
}
class SubClass extends SuperClass {
getSuperProp() {
return this.#superProp;
}
}
// SyntaxError: Private field '#superProp'
// must be declared in an enclosing classUnterklassenbildung mittels extends wird später in diesem Kapitel erklärt. Wie man diese Einschränkung umgehen kann, wird in §29.5.4 „Simulation von geschützter und befreundeter Sichtbarkeit mittels WeakMaps“ erklärt.
Private Slots haben eindeutige Schlüssel, die Symbolen ähneln. Betrachten Sie die folgende frühere Klasse
class MyClass {
#instancePrivateField = 1;
instanceProperty = 2;
getInstanceValues() {
return [
this.#instancePrivateField,
this.instanceProperty,
];
}
}Intern wird das private Feld von MyClass ungefähr so behandelt
let MyClass;
{ // Scope of the body of the class
const instancePrivateFieldKey = Symbol();
MyClass = class {
// Very loose approximation of how this
// works in the language specification
__PrivateElements__ = new Map([
[instancePrivateFieldKey, 1],
]);
instanceProperty = 2;
getInstanceValues() {
return [
this.__PrivateElements__.get(instancePrivateFieldKey),
this.instanceProperty,
];
}
}
}Der Wert von instancePrivateFieldKey wird als privater Name bezeichnet. Wir können private Namen in JavaScript nicht direkt verwenden, sondern nur indirekt über die festen Bezeichner von privaten Feldern, privaten Methoden und privaten Accessoren. Wo die festen Bezeichner öffentlicher Slots (wie getInstanceValues) als Zeichenkettenschlüssel interpretiert werden, beziehen sich die festen Bezeichner privater Slots (wie #instancePrivateField) auf private Namen (ähnlich wie Variablennamen auf Werte verweisen).
Da die Bezeichner privater Slots nicht als Schlüssel verwendet werden, erzeugt die Verwendung desselben Bezeichners in verschiedenen Klassen unterschiedliche Slots (Zeile A und Zeile C)
class Color {
#name; // (A)
constructor(name) {
this.#name = name; // (B)
}
static getName(obj) {
return obj.#name;
}
}
class Person {
#name; // (C)
constructor(name) {
this.#name = name;
}
}
assert.equal(
Color.getName(new Color('green')), 'green'
);
// We can’t access the private slot #name of a Person in line B:
assert.throws(
() => Color.getName(new Person('Jane')),
{
name: 'TypeError',
message: 'Cannot read private member #name from'
+ ' an object whose class did not declare it',
}
);Selbst wenn eine Unterklasse denselben Namen für ein privates Feld verwendet, kollidieren die beiden Namen niemals, da sie auf private Namen verweisen (die immer eindeutig sind). Im folgenden Beispiel kollidiert .#privateField in SuperClass nicht mit .#privateField in SubClass, obwohl beide Slots direkt in inst gespeichert werden
class SuperClass {
#privateField = 'super';
getSuperPrivateField() {
return this.#privateField;
}
}
class SubClass extends SuperClass {
#privateField = 'sub';
getSubPrivateField() {
return this.#privateField;
}
}
const inst = new SubClass();
assert.equal(
inst.getSuperPrivateField(), 'super'
);
assert.equal(
inst.getSubPrivateField(), 'sub'
);Unterklassenbildung mittels extends wird später in diesem Kapitel erklärt.
in, um zu prüfen, ob ein Objekt einen bestimmten privaten Slot hatDer in-Operator kann verwendet werden, um zu prüfen, ob ein privater Slot existiert (Zeile A)
class Color {
#name;
constructor(name) {
this.#name = name;
}
static check(obj) {
return #name in obj; // (A)
}
}Sehen wir uns weitere Beispiele für die Anwendung von in auf private Slots an.
Private Methoden. Der folgende Code zeigt, dass private Methoden private Slots in Instanzen erstellen
class C1 {
#priv() {}
static check(obj) {
return #priv in obj;
}
}
assert.equal(C1.check(new C1()), true);Statische private Felder. Wir können in auch für ein statisches privates Feld verwenden
class C2 {
static #priv = 1;
static check(obj) {
return #priv in obj;
}
}
assert.equal(C2.check(C2), true);
assert.equal(C2.check(new C2()), false);Statische private Methoden. Und wir können den Slot einer statischen privaten Methode prüfen
class C3 {
static #priv() {}
static check(obj) {
return #priv in obj;
}
}
assert.equal(C3.check(C3), true);Verwendung desselben privaten Bezeichners in verschiedenen Klassen. Im folgenden Beispiel haben die beiden Klassen Color und Person beide einen Slot, dessen Bezeichner #name ist. Der in-Operator unterscheidet sie korrekt
class Color {
#name;
constructor(name) {
this.#name = name;
}
static check(obj) {
return #name in obj;
}
}
class Person {
#name;
constructor(name) {
this.#name = name;
}
static check(obj) {
return #name in obj;
}
}
// Detecting Color’s #name
assert.equal(
Color.check(new Color()), true
);
assert.equal(
Color.check(new Person()), false
);
// Detecting Person’s #name
assert.equal(
Person.check(new Person()), true
);
assert.equal(
Person.check(new Color()), false
);Ich empfehle die Verwendung von Klassen aus folgenden Gründen
Klassen sind ein gängiger Standard für Objekterzeugung und Vererbung, der jetzt weithin von Bibliotheken und Frameworks unterstützt wird. Dies ist eine Verbesserung gegenüber früher, als fast jedes Framework seine eigene Vererbungsbibliothek hatte.
Sie helfen Werkzeugen wie IDEs und Typ-Checkern bei ihrer Arbeit und ermöglichen dort neue Funktionen.
Wenn Sie aus einer anderen Sprache zu JavaScript kommen und an Klassen gewöhnt sind, können Sie schneller loslegen.
JavaScript-Engines optimieren sie. Das heißt, Code, der Klassen verwendet, ist fast immer schneller als Code, der eine benutzerdefinierte Vererbungsbibliothek verwendet.
Wir können integrierte Konstruktorfunktionen wie Error als Unterklassen bilden.
Das bedeutet nicht, dass Klassen perfekt sind
Es besteht die Gefahr, die Vererbung zu übertreiben.
Es besteht die Gefahr, zu viel Funktionalität in Klassen zu packen (wenn ein Teil davon besser in Funktionen aufgehoben ist).
Klassen sehen für Programmierer aus anderen Sprachen vertraut aus, funktionieren aber anders und werden anders verwendet (siehe nächster Unterabschnitt). Daher besteht die Gefahr, dass diese Programmierer Code schreiben, der sich nicht wie JavaScript anfühlt.
Wie Klassen oberflächlich erscheinen, ist ganz anders, als sie tatsächlich funktionieren. Anders ausgedrückt gibt es eine Diskrepanz zwischen Syntax und Semantik. Zwei Beispiele sind
C erstellt eine Methode im Objekt C.prototype.Die Motivation für die Diskrepanz ist die Abwärtskompatibilität. Glücklicherweise verursacht die Diskrepanz in der Praxis wenig Probleme; wir sind normalerweise in Ordnung, wenn wir uns an das halten, was Klassen vorgeben zu sein.
Dies war ein erster Blick auf Klassen. Wir werden bald weitere Funktionen untersuchen.
Übung: Eine Klasse schreiben
exercises/classes/point_class_test.mjs
Unter der Haube wird eine Klasse zu zwei verbundenen Objekten. Lassen Sie uns die Klasse Person noch einmal betrachten, um zu sehen, wie das funktioniert
class Person {
#firstName;
constructor(firstName) {
this.#firstName = firstName;
}
describe() {
return `Person named ${this.#firstName}`;
}
static extractNames(persons) {
return persons.map(person => person.#firstName);
}
}Das erste von der Klasse erzeugte Objekt wird in Person gespeichert. Es hat vier Eigenschaften
assert.deepEqual(
Reflect.ownKeys(Person),
['length', 'name', 'prototype', 'extractNames']
);
// The number of parameters of the constructor
assert.equal(
Person.length, 1
);
// The name of the class
assert.equal(
Person.name, 'Person'
);Die beiden verbleibenden Eigenschaften sind
Person.extractNames ist die statische Methode, die wir bereits in Aktion gesehen haben.Person.prototype verweist auf das zweite Objekt, das von einer Klassendefinition erstellt wird.Das sind die Inhalte von Person.prototype
assert.deepEqual(
Reflect.ownKeys(Person.prototype),
['constructor', 'describe']
);Es gibt zwei Eigenschaften
Person.prototype.constructor verweist auf den Konstruktor.Person.prototype.describe ist die Methode, die wir bereits verwendet haben.Das Objekt Person.prototype ist der Prototyp aller Instanzen
const jane = new Person('Jane');
assert.equal(
Object.getPrototypeOf(jane), Person.prototype
);
const tarzan = new Person('Tarzan');
assert.equal(
Object.getPrototypeOf(tarzan), Person.prototype
);Das erklärt, wie die Instanzen ihre Methoden erhalten: Sie erben sie vom Objekt Person.prototype.
Abb. 13 visualisiert, wie alles verbunden ist.
.__proto__ vs. .prototypeEs ist leicht, .__proto__ und .prototype zu verwechseln. Hoffentlich macht Abb. 13 klar, wie sie sich unterscheiden
.__proto__ ist ein Accessor der Klasse Object, der es uns ermöglicht, die Prototypen ihrer Instanzen abzurufen und zu setzen.
.prototype ist eine normale Eigenschaft wie jede andere. Sie ist nur besonders, weil der new-Operator ihren Wert als Prototyp von Instanzen verwendet. Ihr Name ist nicht ideal. Ein anderer Name wie .instancePrototype wäre passender.
Person.prototype.constructor (fortgeschritten)Es gibt ein Detail in Abb. 13, das wir uns noch nicht angesehen haben: Person.prototype.constructor verweist zurück auf Person
> Person.prototype.constructor === Person
trueDiese Einrichtung besteht aus Gründen der Abwärtskompatibilität. Sie hat aber zwei zusätzliche Vorteile.
Erstens erbt jede Instanz einer Klasse die Eigenschaft .constructor. Daher können wir über eine Instanz „ähnliche“ Objekte erstellen
const jane = new Person('Jane');
const cheeta = new jane.constructor('Cheeta');
// cheeta is also an instance of Person
assert.equal(cheeta instanceof Person, true);Zweitens können wir den Namen der Klasse erhalten, die eine gegebene Instanz erstellt hat
const tarzan = new Person('Tarzan');
assert.equal(tarzan.constructor.name, 'Person');In diesem Unterabschnitt lernen wir zwei verschiedene Möglichkeiten kennen, Methoden aufzurufen
Das Verständnis beider wird uns wichtige Einblicke in die Funktionsweise von Methoden geben.
Wir werden die zweite Methode später in diesem Kapitel benötigen: Sie wird es uns ermöglichen, nützliche Methoden von Object.prototype zu „leihen“.
Betrachten wir, wie Methodenaufrufe mit Klassen funktionieren. Wir greifen noch einmal auf jane von früher zurück
class Person {
#firstName;
constructor(firstName) {
this.#firstName = firstName;
}
describe() {
return 'Person named '+this.#firstName;
}
}
const jane = new Person('Jane');Abb. 14 enthält ein Diagramm mit janes Prototypkette.
Normale Methodenaufrufe werden dispatched – der Methodenaufruf
jane.describe()erfolgt in zwei Schritten
Dispatch: JavaScript durchläuft die Prototypkette, beginnend mit jane, um das erste Objekt zu finden, das eine eigene Eigenschaft mit dem Schlüssel 'describe' hat: Es schaut zuerst bei jane nach und findet keine eigene Eigenschaft .describe. Es fährt mit dem Prototyp von jane, Person.prototype, fort und findet eine eigene Eigenschaft describe, deren Wert es zurückgibt.
const func = jane.describe;Aufruf: Das Aufrufen einer Methode über ihren Wert unterscheidet sich vom Aufrufen einer Funktion über ihren Wert darin, dass es nicht nur das aufgerufen wird, was vor den Klammern steht, mit den Argumenten in den Klammern, sondern auch this auf den Empfänger des Methodenaufrufs (in diesem Fall jane) gesetzt wird.
func.call(jane);Diese Art der dynamischen Suche nach einer Methode und ihres Aufrufs wird als dynamischer Dispatch bezeichnet.
Wir können Methodenaufrufe auch direkt, ohne Dispatching, vornehmen
Person.prototype.describe.call(jane)Diesmal zeigen wir direkt auf die Methode über Person.prototype.describe und suchen nicht in der Prototypkette danach. Wir geben auch this anders an – über .call().
this zeigt immer auf die Instanz
Unabhängig davon, wo sich eine Methode in der Prototypkette einer Instanz befindet, zeigt this immer auf die Instanz (den Anfang der Prototypkette). Dies ermöglicht es .describe(), auf .#firstName im Beispiel zuzugreifen.
Wann sind direkte Methodenaufrufe nützlich? Immer dann, wenn wir eine Methode von anderswo „leihen“ wollen, die ein bestimmtes Objekt nicht hat – zum Beispiel
const obj = Object.create(null);
// `obj` is not an instance of Object and doesn’t inherit
// its prototype method .toString()
assert.throws(
() => obj.toString(),
/^TypeError: obj.toString is not a function$/
);
assert.equal(
Object.prototype.toString.call(obj),
'[object Object]'
);Vor ECMAScript 6 hatte JavaScript keine Klassen. Stattdessen wurden gewöhnliche Funktionen als Konstruktorfunktionen verwendet
function StringBuilderConstr(initialString) {
this.string = initialString;
}
StringBuilderConstr.prototype.add = function (str) {
this.string += str;
return this;
};
const sb = new StringBuilderConstr('¡');
sb.add('Hola').add('!');
assert.equal(
sb.string, '¡Hola!'
);Klassen bieten eine bessere Syntax für diesen Ansatz
class StringBuilderClass {
constructor(initialString) {
this.string = initialString;
}
add(str) {
this.string += str;
return this;
}
}
const sb = new StringBuilderClass('¡');
sb.add('Hola').add('!');
assert.equal(
sb.string, '¡Hola!'
);Die Unterklassenbildung ist mit Konstruktorfunktionen besonders knifflig. Klassen bieten auch Vorteile, die über eine bequemere Syntax hinausgehen
Error können als Unterklassen gebildet werden.super aufrufen.new aufgerufen werden und haben nicht die Eigenschaft .prototype.Klassen sind so kompatibel mit Konstruktorfunktionen, dass sie diese sogar erweitern können
function SuperConstructor() {}
class SubClass extends SuperConstructor {}
assert.equal(
new SubClass() instanceof SuperConstructor, true
);extends und Unterklassenbildung werden später in diesem Kapitel erklärt.
Das bringt uns zu einer interessanten Erkenntnis. Einerseits bezieht sich StringBuilderClass über StringBuilderClass.prototype.constructor auf seinen Konstruktor.
Andererseits ist die Klasse der Konstruktor (eine Funktion)
> StringBuilderClass.prototype.constructor === StringBuilderClass
true
> typeof StringBuilderClass
'function' Konstruktor (Funktionen) vs. Klassen
Aufgrund ihrer Ähnlichkeit verwende ich die Begriffe Konstruktor (Funktion) und Klasse austauschbar.
Alle Mitglieder im Körper der folgenden Klassendeklaration erstellen Eigenschaften von PublicProtoClass.prototype.
class PublicProtoClass {
constructor(args) {
// (Do something with `args` here.)
}
publicProtoMethod() {
return 'publicProtoMethod';
}
get publicProtoAccessor() {
return 'publicProtoGetter';
}
set publicProtoAccessor(value) {
assert.equal(value, 'publicProtoSetter');
}
}
assert.deepEqual(
Reflect.ownKeys(PublicProtoClass.prototype),
['constructor', 'publicProtoMethod', 'publicProtoAccessor']
);
const inst = new PublicProtoClass('arg1', 'arg2');
assert.equal(
inst.publicProtoMethod(), 'publicProtoMethod'
);
assert.equal(
inst.publicProtoAccessor, 'publicProtoGetter'
);
inst.publicProtoAccessor = 'publicProtoSetter';const accessorKey = Symbol('accessorKey');
const syncMethodKey = Symbol('syncMethodKey');
const syncGenMethodKey = Symbol('syncGenMethodKey');
const asyncMethodKey = Symbol('asyncMethodKey');
const asyncGenMethodKey = Symbol('asyncGenMethodKey');
class PublicProtoClass2 {
// Identifier keys
get accessor() {}
set accessor(value) {}
syncMethod() {}
* syncGeneratorMethod() {}
async asyncMethod() {}
async * asyncGeneratorMethod() {}
// Quoted keys
get 'an accessor'() {}
set 'an accessor'(value) {}
'sync method'() {}
* 'sync generator method'() {}
async 'async method'() {}
async * 'async generator method'() {}
// Computed keys
get [accessorKey]() {}
set [accessorKey](value) {}
[syncMethodKey]() {}
* [syncGenMethodKey]() {}
async [asyncMethodKey]() {}
async * [asyncGenMethodKey]() {}
}
// Quoted and computed keys are accessed via square brackets:
const inst = new PublicProtoClass2();
inst['sync method']();
inst[syncMethodKey]();Anführungszeichen- und berechnete Schlüssel können auch in Objektliteralen verwendet werden
Weitere Informationen zu Accessoren (definiert über Getter und/oder Setter), Generatoren, asynchronen Methoden und asynchronen Generator-Methoden
Private Methoden (und Accessoren) sind eine interessante Mischung aus Prototyp-Mitgliedern und Instanz-Mitgliedern.
Einerseits werden private Methoden in Slots in Instanzen gespeichert (Zeile A)
class MyClass {
#privateMethod() {}
static check() {
const inst = new MyClass();
assert.equal(
#privateMethod in inst, true // (A)
);
assert.equal(
#privateMethod in MyClass.prototype, false
);
assert.equal(
#privateMethod in MyClass, false
);
}
}
MyClass.check();Warum werden sie nicht in .prototype-Objekten gespeichert? Private Slots werden nicht vererbt, nur Eigenschaften.
Andererseits werden private Methoden zwischen Instanzen geteilt – wie öffentliche Prototyp-Methoden
class MyClass {
#privateMethod() {}
static check() {
const inst1 = new MyClass();
const inst2 = new MyClass();
assert.equal(
inst1.#privateMethod,
inst2.#privateMethod
);
}
}Aufgrund dessen und weil ihre Syntax der von öffentlichen Prototyp-Methoden ähnelt, werden sie hier behandelt.
Der folgende Code demonstriert, wie private Methoden und Accessoren funktionieren
class PrivateMethodClass {
#privateMethod() {
return 'privateMethod';
}
get #privateAccessor() {
return 'privateGetter';
}
set #privateAccessor(value) {
assert.equal(value, 'privateSetter');
}
callPrivateMembers() {
assert.equal(this.#privateMethod(), 'privateMethod');
assert.equal(this.#privateAccessor, 'privateGetter');
this.#privateAccessor = 'privateSetter';
}
}
assert.deepEqual(
Reflect.ownKeys(new PrivateMethodClass()), []
);Bei privaten Slots sind die Schlüssel immer Bezeichner
class PrivateMethodClass2 {
get #accessor() {}
set #accessor(value) {}
#syncMethod() {}
* #syncGeneratorMethod() {}
async #asyncMethod() {}
async * #asyncGeneratorMethod() {}
}Weitere Informationen zu Accessoren (definiert über Getter und/oder Setter), Generatoren, asynchronen Methoden und asynchronen Generator-Methoden
Instanzen der folgenden Klasse haben zwei Instanz-Eigenschaften (erstellt in Zeile A und Zeile B)
class InstPublicClass {
// Instance public field
instancePublicField = 0; // (A)
constructor(value) {
// We don’t need to mention .property elsewhere!
this.property = value; // (B)
}
}
const inst = new InstPublicClass('constrArg');
assert.deepEqual(
Reflect.ownKeys(inst),
['instancePublicField', 'property']
);
assert.equal(
inst.instancePublicField, 0
);
assert.equal(
inst.property, 'constrArg'
);Wenn wir eine Instanzeigenschaft innerhalb des Konstruktors erstellen (Zeile B), müssen wir sie nicht woanders „deklarieren“. Wie wir bereits gesehen haben, ist das bei Instanz-privaten Feldern anders.
Beachten Sie, dass Instanz-Eigenschaften in JavaScript relativ häufig vorkommen; viel häufiger als z. B. in Java, wo die meisten Instanzzustände privat sind.
const computedFieldKey = Symbol('computedFieldKey');
class InstPublicClass2 {
'quoted field key' = 1;
[computedFieldKey] = 2;
}
const inst = new InstPublicClass2();
assert.equal(inst['quoted field key'], 1);
assert.equal(inst[computedFieldKey], 2);this in Instanz-öffentlichen Feldern? (fortgeschritten)Im Initialisierer eines Instanz-öffentlichen Feldes bezieht sich this auf die neu erstellte Instanz
class MyClass {
instancePublicField = this;
}
const inst = new MyClass();
assert.equal(
inst.instancePublicField, inst
);Die Ausführung von Instanz-öffentlichen Feldern folgt ungefähr diesen beiden Regeln
super() aufgerufen wird.super() ausgeführt.Das folgende Beispiel demonstriert diese Regeln
class SuperClass {
superProp = console.log('superProp');
constructor() {
console.log('super-constructor');
}
}
class SubClass extends SuperClass {
subProp = console.log('subProp');
constructor() {
console.log('BEFORE super()');
super();
console.log('AFTER super()');
}
}
new SubClass();
// Output:
// 'BEFORE super()'
// 'superProp'
// 'super-constructor'
// 'subProp'
// 'AFTER super()'extends und Unterklassenbildung werden später in diesem Kapitel erklärt.
Die folgende Klasse enthält zwei Instanz-private Felder (Zeile A und Zeile B)
class InstPrivateClass {
#privateField1 = 'private field 1'; // (A)
#privateField2; // (B) required!
constructor(value) {
this.#privateField2 = value; // (C)
}
/**
* Private fields are not accessible outside the class body.
*/
checkPrivateValues() {
assert.equal(
this.#privateField1, 'private field 1'
);
assert.equal(
this.#privateField2, 'constructor argument'
);
}
}
const inst = new InstPrivateClass('constructor argument');
inst.checkPrivateValues();
// No instance properties were created
assert.deepEqual(
Reflect.ownKeys(inst),
[]
);Beachten Sie, dass wir .#privateField2 in Zeile C nur verwenden können, wenn wir es im Klassenkörper deklarieren.
In diesem Abschnitt sehen wir uns zwei Techniken an, um Instanzdaten privat zu halten. Da sie nicht auf Klassen basieren, können wir sie auch für Objekte verwenden, die auf andere Weise erstellt wurden – z. B. über Objektliterale.
Die erste Technik macht eine Eigenschaft privat, indem sie ihr einen Unterstrich voranstellt. Dies schützt die Eigenschaft in keiner Weise; es signalisiert lediglich nach außen: „Sie müssen diese Eigenschaft nicht kennen.“
Im folgenden Code sind die Eigenschaften ._counter und ._action privat.
class Countdown {
constructor(counter, action) {
this._counter = counter;
this._action = action;
}
dec() {
this._counter--;
if (this._counter === 0) {
this._action();
}
}
}
// The two properties aren’t really private:
assert.deepEqual(
Object.keys(new Countdown()),
['_counter', '_action']);Mit dieser Technik erhalten wir keinen Schutz und private Namen können kollidieren. Auf der positiven Seite ist sie einfach zu verwenden.
Private Methoden funktionieren ähnlich: Es sind normale Methoden, deren Namen mit Unterstrichen beginnen.
Wir können private Instanzdaten auch über WeakMaps verwalten
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);
counter--;
_counter.set(this, counter);
if (counter === 0) {
_action.get(this)();
}
}
}
// The two pseudo-properties are truly private:
assert.deepEqual(
Object.keys(new Countdown()),
[]);Wie genau das funktioniert, wird im Kapitel über WeakMaps erklärt.
Diese Technik bietet uns erheblichen Schutz vor externem Zugriff und es kann keine Namenskollisionen geben. Aber sie ist auch komplizierter zu verwenden.
Wir steuern die Sichtbarkeit der Pseudo-Eigenschaft _superProp, indem wir steuern, wer Zugriff darauf hat – zum Beispiel: Wenn die Variable innerhalb eines Moduls existiert und nicht exportiert wird, können alle innerhalb des Moduls und niemand außerhalb des Moduls darauf zugreifen. Mit anderen Worten: Der Geltungsbereich der Privatsphäre ist in diesem Fall nicht die Klasse, sondern das Modul. Wir könnten den Bereich jedoch einschränken
let Countdown;
{ // class scope
const _counter = new WeakMap();
const _action = new WeakMap();
Countdown = class {
// ···
}
}Diese Technik unterstützt private Methoden nicht wirklich. Aber Modul-lokale Funktionen, die Zugriff auf _superProp haben, sind das Nächstbeste
const _counter = new WeakMap();
const _action = new WeakMap();
class Countdown {
constructor(counter, action) {
_counter.set(this, counter);
_action.set(this, action);
}
dec() {
privateDec(this);
}
}
function privateDec(_this) { // (A)
let counter = _counter.get(_this);
counter--;
_counter.set(_this, counter);
if (counter === 0) {
_action.get(_this)();
}
}Beachten Sie, dass this zum expliziten Funktionsparameter _this wird (Zeile A).
Wie bereits erörtert, sind Instanz-private Felder nur innerhalb ihrer Klassen sichtbar und nicht einmal in Unterklassen. Daher gibt es keine eingebaute Möglichkeit,
Im vorherigen Unterabschnitt haben wir „Modulsichtbarkeit“ simuliert (jeder innerhalb eines Moduls hat Zugriff auf Instanzdaten) über WeakMaps. Daher
Das nächste Beispiel demonstriert geschützte Sichtbarkeit
const _superProp = new WeakMap();
class SuperClass {
constructor() {
_superProp.set(this, 'superProp');
}
}
class SubClass extends SuperClass {
getSuperProp() {
return _superProp.get(this);
}
}
assert.equal(
new SubClass().getSuperProp(),
'superProp'
);Unterklassenbildung mittels extends wird später in diesem Kapitel erklärt.
Alle Mitglieder im Körper der folgenden Klassendeklaration erstellen sogenannte statische Eigenschaften – Eigenschaften von StaticClass selbst.
class StaticPublicMethodsClass {
static staticMethod() {
return 'staticMethod';
}
static get staticAccessor() {
return 'staticGetter';
}
static set staticAccessor(value) {
assert.equal(value, 'staticSetter');
}
}
assert.equal(
StaticPublicMethodsClass.staticMethod(), 'staticMethod'
);
assert.equal(
StaticPublicMethodsClass.staticAccessor, 'staticGetter'
);
StaticPublicMethodsClass.staticAccessor = 'staticSetter';const accessorKey = Symbol('accessorKey');
const syncMethodKey = Symbol('syncMethodKey');
const syncGenMethodKey = Symbol('syncGenMethodKey');
const asyncMethodKey = Symbol('asyncMethodKey');
const asyncGenMethodKey = Symbol('asyncGenMethodKey');
class StaticPublicMethodsClass2 {
// Identifier keys
static get accessor() {}
static set accessor(value) {}
static syncMethod() {}
static * syncGeneratorMethod() {}
static async asyncMethod() {}
static async * asyncGeneratorMethod() {}
// Quoted keys
static get 'an accessor'() {}
static set 'an accessor'(value) {}
static 'sync method'() {}
static * 'sync generator method'() {}
static async 'async method'() {}
static async * 'async generator method'() {}
// Computed keys
static get [accessorKey]() {}
static set [accessorKey](value) {}
static [syncMethodKey]() {}
static * [syncGenMethodKey]() {}
static async [asyncMethodKey]() {}
static async * [asyncGenMethodKey]() {}
}
// Quoted and computed keys are accessed via square brackets:
StaticPublicMethodsClass2['sync method']();
StaticPublicMethodsClass2[syncMethodKey]();Anführungszeichen- und berechnete Schlüssel können auch in Objektliteralen verwendet werden
Weitere Informationen zu Accessoren (definiert über Getter und/oder Setter), Generatoren, asynchronen Methoden und asynchronen Generator-Methoden
Der folgende Code demonstriert statische öffentliche Felder. StaticPublicFieldClass hat drei davon
const computedFieldKey = Symbol('computedFieldKey');
class StaticPublicFieldClass {
static identifierFieldKey = 1;
static 'quoted field key' = 2;
static [computedFieldKey] = 3;
}
assert.deepEqual(
Reflect.ownKeys(StaticPublicFieldClass),
[
'length', // number of constructor parameters
'name', // 'StaticPublicFieldClass'
'prototype',
'identifierFieldKey',
'quoted field key',
computedFieldKey,
],
);
assert.equal(StaticPublicFieldClass.identifierFieldKey, 1);
assert.equal(StaticPublicFieldClass['quoted field key'], 2);
assert.equal(StaticPublicFieldClass[computedFieldKey], 3);Die folgende Klasse hat zwei statische private Slots (Zeile A und Zeile B)
class StaticPrivateClass {
// Declare and initialize
static #staticPrivateField = 'hello'; // (A)
static #twice() { // (B)
const str = StaticPrivateClass.#staticPrivateField;
return str + ' ' + str;
}
static getResultOfTwice() {
return StaticPrivateClass.#twice();
}
}
assert.deepEqual(
Reflect.ownKeys(StaticPrivateClass),
[
'length', // number of constructor parameters
'name', // 'StaticPublicFieldClass'
'prototype',
'getResultOfTwice',
],
);
assert.equal(
StaticPrivateClass.getResultOfTwice(),
'hello hello'
);Dies ist eine vollständige Liste aller Arten von statischen privaten Slots
class MyClass {
static #staticPrivateMethod() {}
static * #staticPrivateGeneratorMethod() {}
static async #staticPrivateAsyncMethod() {}
static async * #staticPrivateAsyncGeneratorMethod() {}
static get #staticPrivateAccessor() {}
static set #staticPrivateAccessor(value) {}
}Um Instanzdaten über Klassen einzurichten, haben wir zwei Konstrukte
Für statische Daten haben wir
Der folgende Code demonstriert statische Blöcke (Zeile A)
class Translator {
static translations = {
yes: 'ja',
no: 'nein',
maybe: 'vielleicht',
};
static englishWords = [];
static germanWords = [];
static { // (A)
for (const [english, german] of Object.entries(this.translations)) {
this.englishWords.push(english);
this.germanWords.push(german);
}
}
}Wir könnten den Code im statischen Block auch nach der Klasse (auf oberster Ebene) ausführen. Die Verwendung eines statischen Blocks hat jedoch zwei Vorteile
Die Regeln für die Funktionsweise statischer Initialisierungsblöcke sind relativ einfach
Der folgende Code demonstriert diese Regeln
class SuperClass {
static superField1 = console.log('superField1');
static {
assert.equal(this, SuperClass);
console.log('static block 1 SuperClass');
}
static superField2 = console.log('superField2');
static {
console.log('static block 2 SuperClass');
}
}
class SubClass extends SuperClass {
static subField1 = console.log('subField1');
static {
assert.equal(this, SubClass);
console.log('static block 1 SubClass');
}
static subField2 = console.log('subField2');
static {
console.log('static block 2 SubClass');
}
}
// Output:
// 'superField1'
// 'static block 1 SuperClass'
// 'superField2'
// 'static block 2 SuperClass'
// 'subField1'
// 'static block 1 SubClass'
// 'subField2'
// 'static block 2 SubClass'Unterklassenbildung mittels extends wird später in diesem Kapitel erklärt.
this zum Zugriff auf statische private FelderIn statischen öffentlichen Mitgliedern können wir statische öffentliche Slots über this aufrufen. Leider sollten wir ihn nicht zum Aufrufen statischer privater Slots verwenden.
this und statische öffentliche FelderBetrachten Sie den folgenden Code
class SuperClass {
static publicData = 1;
static getPublicViaThis() {
return this.publicData;
}
}
class SubClass extends SuperClass {
}Unterklassenbildung mittels extends wird später in diesem Kapitel erklärt.
Statische öffentliche Felder sind Eigenschaften. Wenn wir den Methodenaufruf machen
assert.equal(SuperClass.getPublicViaThis(), 1);dann zeigt this auf SuperClass und alles funktioniert wie erwartet. Wir können .getPublicViaThis() auch über die Unterklasse aufrufen
assert.equal(SubClass.getPublicViaThis(), 1);SubClass erbt .getPublicViaThis() von seinem Prototyp SuperClass. this zeigt auf SubClass und die Dinge funktionieren weiterhin, weil SubClass auch die Eigenschaft .publicData erbt.
Nebenbei bemerkt: Wenn wir in getPublicViaThis() this.publicData zugewiesen hätten und es über SubClass.getPublicViaThis() aufgerufen hätten, würden wir eine neue eigene Eigenschaft von SubClass erstellen, die die von SuperClass geerbte Eigenschaft (nicht-destruktiv) überschreibt.
this und statische private FelderBetrachten Sie den folgenden Code
class SuperClass {
static #privateData = 2;
static getPrivateDataViaThis() {
return this.#privateData;
}
static getPrivateDataViaClassName() {
return SuperClass.#privateData;
}
}
class SubClass extends SuperClass {
}Der Aufruf von .getPrivateDataViaThis() über SuperClass funktioniert, da this auf SuperClass zeigt
assert.equal(SuperClass.getPrivateDataViaThis(), 2);Das Aufrufen von .getPrivateDataViaThis() über SubClass funktioniert jedoch nicht, da this nun auf SubClass verweist und SubClass kein statisches privates Feld .#privateData hat (private Slots in Prototypketten werden nicht vererbt).
assert.throws(
() => SubClass.getPrivateDataViaThis(),
{
name: 'TypeError',
message: 'Cannot read private member #privateData from'
+ ' an object whose class did not declare it',
}
);Der Workaround besteht darin, .#privateData direkt über SuperClass anzusprechen.
assert.equal(SubClass.getPrivateDataViaClassName(), 2);Bei statischen privaten Methoden stehen wir vor demselben Problem.
Jeder Member innerhalb einer Klasse kann auf alle anderen Member innerhalb dieser Klasse zugreifen – sowohl öffentliche als auch private.
class DemoClass {
static #staticPrivateField = 1;
#instPrivField = 2;
static staticMethod(inst) {
// A static method can access static private fields
// and instance private fields
assert.equal(DemoClass.#staticPrivateField, 1);
assert.equal(inst.#instPrivField, 2);
}
protoMethod() {
// A prototype method can access instance private fields
// and static private fields
assert.equal(this.#instPrivField, 2);
assert.equal(DemoClass.#staticPrivateField, 1);
}
}Im Gegensatz dazu kann niemand außerhalb auf die privaten Member zugreifen.
// Accessing private fields outside their classes triggers
// syntax errors (before the code is even executed).
assert.throws(
() => eval('DemoClass.#staticPrivateField'),
{
name: 'SyntaxError',
message: "Private field '#staticPrivateField' must"
+ " be declared in an enclosing class",
}
);
// Accessing private fields outside their classes triggers
// syntax errors (before the code is even executed).
assert.throws(
() => eval('new DemoClass().#instPrivField'),
{
name: 'SyntaxError',
message: "Private field '#instPrivField' must"
+ " be declared in an enclosing class",
}
);Der folgende Code funktioniert nur in ES2022 – aufgrund jeder Zeile, die ein Hash-Symbol (#) enthält.
class StaticClass {
static #secret = 'Rumpelstiltskin';
static #getSecretInParens() {
return `(${StaticClass.#secret})`;
}
static callStaticPrivateMethod() {
return StaticClass.#getSecretInParens();
}
}Da private Slots nur einmal pro Klasse existieren, können wir #secret und #getSecretInParens in den Scope verschieben, der die Klasse umgibt, und ein Modul verwenden, um sie von der Außenwelt des Moduls zu verbergen.
const secret = 'Rumpelstiltskin';
function getSecretInParens() {
return `(${secret})`;
}
// Only the class is accessible outside the module
export class StaticClass {
static callStaticPrivateMethod() {
return getSecretInParens();
}
}Manchmal gibt es mehrere Möglichkeiten, wie eine Klasse instanziiert werden kann. Dann können wir statische Factory-Methoden implementieren, wie z. B. Point.fromPolar().
class Point {
static fromPolar(radius, angle) {
const x = radius * Math.cos(angle);
const y = radius * Math.sin(angle);
return new Point(x, y);
}
constructor(x=0, y=0) {
this.x = x;
this.y = y;
}
}
assert.deepEqual(
Point.fromPolar(13, 0.39479111969976155),
new Point(12, 5)
);Ich mag, wie deskriptiv statische Factory-Methoden sind: fromPolar beschreibt, wie eine Instanz erstellt wird. Die Standardbibliothek von JavaScript hat auch solche Factory-Methoden – zum Beispiel
Array.from()Object.create()Ich bevorzuge es, entweder keine statischen Factory-Methoden zu haben oder nur statische Factory-Methoden. Dinge, die im letzteren Fall zu beachten sind
Im folgenden Code verwenden wir ein geheimes Token (Zeile A), um den Aufruf des Konstruktors von außerhalb des aktuellen Moduls zu verhindern.
// Only accessible inside the current module
const secretToken = Symbol('secretToken'); // (A)
export class Point {
static create(x=0, y=0) {
return new Point(secretToken, x, y);
}
static fromPolar(radius, angle) {
const x = radius * Math.cos(angle);
const y = radius * Math.sin(angle);
return new Point(secretToken, x, y);
}
constructor(token, x, y) {
if (token !== secretToken) {
throw new TypeError('Must use static factory method');
}
this.x = x;
this.y = y;
}
}
Point.create(3, 4); // OK
assert.throws(
() => new Point(3, 4),
TypeError
);Klassen können auch bestehende Klassen erweitern. Zum Beispiel erweitert die folgende Klasse Employee die Klasse Person.
class Person {
#firstName;
constructor(firstName) {
this.#firstName = firstName;
}
describe() {
return `Person named ${this.#firstName}`;
}
static extractNames(persons) {
return persons.map(person => person.#firstName);
}
}
class Employee extends Person {
constructor(firstName, title) {
super(firstName);
this.title = title;
}
describe() {
return super.describe() +
` (${this.title})`;
}
}
const jane = new Employee('Jane', 'CTO');
assert.equal(
jane.title,
'CTO'
);
assert.equal(
jane.describe(),
'Person named Jane (CTO)'
);Terminologie im Zusammenhang mit dem Erweitern
Person ist die Superklasse von Employee.Employee ist die Unterklasse von Person.Innerhalb des .constructor() einer abgeleiteten Klasse müssen wir den Super-Konstruktor über super() aufrufen, bevor wir auf this zugreifen können. Warum ist das so?
Betrachten wir eine Kette von Klassen
AB erweitert A.C erweitert B.Wenn wir new C() aufrufen, ruft der Konstruktor von C rekursiv den Konstruktor von B auf, der wiederum rekursiv den Konstruktor von A aufruft. Instanzen werden immer in Basisklassen erstellt, bevor die Konstruktoren von Unterklassen ihre Slots hinzufügen. Daher existiert die Instanz noch nicht, bevor wir super() aufrufen, und wir können noch nicht über this darauf zugreifen.
Beachten Sie, dass statische öffentliche Slots vererbt werden. Zum Beispiel erbt Employee die statische Methode .extractNames().
> 'extractNames' in Employee
true Übung: Unterklassenbildung
exercises/classes/color_point_class_test.mjs
Die Klassen Person und Employee aus dem vorherigen Abschnitt bestehen aus mehreren Objekten (Abb. 15). Eine Schlüssel Erkenntnis zum Verständnis, wie diese Objekte miteinander verbunden sind, ist, dass es zwei Prototypketten gibt:
Die Instanz-Prototypkette beginnt mit jane und geht weiter über Employee.prototype und Person.prototype. Prinzipiell endet die Prototypkette an dieser Stelle, aber wir erhalten noch ein weiteres Objekt: Object.prototype. Dieser Prototyp stellt Dienste für praktisch alle Objekte bereit, weshalb er hier ebenfalls enthalten ist.
> Object.getPrototypeOf(Person.prototype) === Object.prototype
trueIn der Klassen-Prototypkette kommt zuerst Employee, dann Person. Danach geht die Kette weiter mit Function.prototype, die nur da ist, weil Person eine Funktion ist und Funktionen die Dienste von Function.prototype benötigen.
> Object.getPrototypeOf(Person) === Function.prototype
trueinstanceof und Unterklassenbildung (fortgeschritten)Wir haben noch nicht gelernt, wie instanceof wirklich funktioniert. Wie ermittelt instanceof, ob ein Wert x eine Instanz einer Klasse C ist (es kann eine direkte Instanz von C oder eine direkte Instanz einer Unterklasse von C sein)? Es prüft, ob C.prototype in der Prototypkette von x vorhanden ist. Das heißt, die folgenden beiden Ausdrücke sind äquivalent:
x instanceof C
C.prototype.isPrototypeOf(x)Wenn wir zu Abb. 15 zurückkehren, können wir bestätigen, dass die Prototypkette uns zu folgenden korrekten Antworten führt:
> jane instanceof Employee
true
> jane instanceof Person
true
> jane instanceof Object
trueBeachten Sie, dass instanceof immer false zurückgibt, wenn seine linke Seite ein primitiver Wert ist.
> 'abc' instanceof String
false
> 123 instanceof Number
falseObject (fortgeschritten)Ein Objekt (ein nicht-primitiver Wert) ist nur dann eine Instanz von Object, wenn Object.prototype in seiner Prototypkette vorhanden ist (siehe vorheriger Unterabschnitt im Link). Praktisch alle Objekte sind Instanzen von Object – zum Beispiel:
assert.equal(
{a: 1} instanceof Object, true
);
assert.equal(
['a'] instanceof Object, true
);
assert.equal(
/abc/g instanceof Object, true
);
assert.equal(
new Map() instanceof Object, true
);
class C {}
assert.equal(
new C() instanceof Object, true
);Im nächsten Beispiel sind obj1 und obj2 beide Objekte (Zeile A und Zeile C), aber sie sind keine Instanzen von Object (Zeile B und Zeile D): Object.prototype ist nicht in ihren Prototypketten, da sie keine Prototypen haben.
const obj1 = {__proto__: null};
assert.equal(
typeof obj1, 'object' // (A)
);
assert.equal(
obj1 instanceof Object, false // (B)
);
const obj2 = Object.create(null);
assert.equal(
typeof obj2, 'object' // (C)
);
assert.equal(
obj2 instanceof Object, false // (D)
);Object.prototype ist das Objekt, das die meisten Prototypketten beendet. Sein Prototyp ist null, was bedeutet, dass es selbst keine Instanz von Object ist.
> typeof Object.prototype
'object'
> Object.getPrototypeOf(Object.prototype)
null
> Object.prototype instanceof Object
falseAls Nächstes werden wir unser Wissen über Unterklassenbildung nutzen, um die Prototypketten einiger integrierter Objekte zu verstehen. Die folgende Hilfsfunktion p() hilft uns bei unseren Erkundungen.
const p = Object.getPrototypeOf.bind(Object);Wir extrahierten die Methode .getPrototypeOf() von Object und wiesen sie p zu.
{}Beginnen wir mit der Untersuchung von einfachen Objekten.
> p({}) === Object.prototype
true
> p(p({})) === null
trueAbb. 16 zeigt ein Diagramm für diese Prototypkette. Wir können sehen, dass {} wirklich eine Instanz von Object ist – Object.prototype befindet sich in seiner Prototypkette.
[]Wie sieht die Prototypkette eines Arrays aus?
> p([]) === Array.prototype
true
> p(p([])) === Object.prototype
true
> p(p(p([]))) === null
trueDiese Prototypkette (visualisiert in Abb. 17) besagt, dass ein Array-Objekt eine Instanz von Array und von Object ist.
function () {}Schließlich zeigt die Prototypkette einer gewöhnlichen Funktion, dass alle Funktionen Objekte sind.
> p(function () {}) === Function.prototype
true
> p(p(function () {})) === Object.prototype
trueDer Prototyp einer Basisklasse ist Function.prototype, was bedeutet, dass sie eine Funktion ist (eine Instanz von Function).
class A {}
assert.equal(
Object.getPrototypeOf(A),
Function.prototype
);
assert.equal(
Object.getPrototypeOf(class {}),
Function.prototype
);Der Prototyp einer abgeleiteten Klasse ist ihre Superklasse.
class B extends A {}
assert.equal(
Object.getPrototypeOf(B),
A
);
assert.equal(
Object.getPrototypeOf(class extends Object {}),
Object
);Interessanterweise sind Object, Array und Function alles Basisklassen.
> Object.getPrototypeOf(Object) === Function.prototype
true
> Object.getPrototypeOf(Array) === Function.prototype
true
> Object.getPrototypeOf(Function) === Function.prototype
trueWie wir jedoch gesehen haben, haben selbst die Instanzen von Basisklassen Object.prototype in ihren Prototypketten, da es Dienste bereitstellt, die alle Objekte benötigen.
Warum sind
Array und Function Basisklassen?
Basisklassen sind dort, wo Instanzen tatsächlich erstellt werden. Sowohl Array als auch Function müssen ihre eigenen Instanzen erstellen, da sie sogenannte „interne Slots“ haben, die nachträglich nicht zu von Object erstellten Instanzen hinzugefügt werden können.
Das Klassensystem von JavaScript unterstützt nur einfache Vererbung. Das heißt, jede Klasse kann höchstens eine Superklasse haben. Ein Weg, diese Einschränkung zu umgehen, ist die Verwendung einer Technik namens Mixin-Klassen (kurz: Mixins).
Die Idee ist wie folgt: Nehmen wir an, wir möchten, dass eine Klasse C von zwei Superklassen S1 und S2 erbt. Das wäre mehrfache Vererbung, die JavaScript nicht unterstützt.
Unser Workaround besteht darin, S1 und S2 zu Mixins, Fabriken für Unterklassen, zu machen.
const S1 = (Sup) => class extends Sup { /*···*/ };
const S2 = (Sup) => class extends Sup { /*···*/ };Jede dieser beiden Funktionen gibt eine Klasse zurück, die eine gegebene Superklasse Sup erweitert. Wir erstellen die Klasse C wie folgt:
class C extends S2(S1(Object)) {
/*···*/
}Wir haben nun eine Klasse C, die die von S2() zurückgegebene Klasse erweitert, die die von S1() zurückgegebene Klasse erweitert, die Object erweitert.
Wir implementieren ein Mixin Branded, das Hilfsmethoden zum Setzen und Abrufen der Marke eines Objekts hat.
const Named = (Sup) => class extends Sup {
name = '(Unnamed)';
toString() {
const className = this.constructor.name;
return `${className} named ${this.name}`;
}
};Wir verwenden dieses Mixin, um eine Klasse City zu implementieren, die einen Namen hat.
class City extends Named(Object) {
constructor(name) {
super();
this.name = name;
}
}Der folgende Code bestätigt, dass das Mixin funktioniert.
const paris = new City('Paris');
assert.equal(
paris.name, 'Paris'
);
assert.equal(
paris.toString(), 'City named Paris'
);Mixins befreien uns von den Einschränkungen der einfachen Vererbung.
Object.prototype (fortgeschritten)Wie wir in §29.7.3 „Nicht alle Objekte sind Instanzen von Object“ gesehen haben, sind fast alle Objekte Instanzen von Object. Diese Klasse stellt mehrere nützliche Methoden und einen Accessor für ihre Instanzen bereit.
+-Operator): Die folgenden Methoden haben Standardimplementierungen, werden aber oft in Unterklassen oder Instanzen überschrieben..toString(): Konfiguriert, wie ein Objekt in einen String umgewandelt wird..toLocaleString(): Eine Version von .toString(), die auf verschiedene Weise über Argumente (Sprache, Region usw.) konfiguriert werden kann..valueOf(): Konfiguriert, wie ein Objekt in einen nicht-String primitiven Wert (oft eine Zahl) umgewandelt wird..isPrototypeOf(): Befindet sich der Receiver in der Prototypkette eines gegebenen Objekts?.propertyIsEnumerable(): Hat der Receiver eine aufzählbare eigene Eigenschaft mit dem gegebenen Schlüssel?.__proto__: Ruft den Prototyp des Receivers ab und setzt ihn.Object.getPrototypeOf()Object.setPrototypeOf().hasOwnProperty(): Hat der Receiver eine eigene Eigenschaft mit einem gegebenen Schlüssel?Object.hasOwn().Bevor wir uns jedes dieser Features genauer ansehen, lernen wir einen wichtigen Fallstrick kennen (und wie man ihn umgeht): Wir können die Features von Object.prototype nicht mit allen Objekten verwenden.
Object.prototype-MethodenDas Aufrufen einer der Methoden von Object.prototype auf einem beliebigen Objekt funktioniert nicht immer. Um zu veranschaulichen, warum, verwenden wir die Methode Object.prototype.hasOwnProperty, die true zurückgibt, wenn ein Objekt eine eigene Eigenschaft mit einem gegebenen Schlüssel hat.
> {ownProp: true}.hasOwnProperty('ownProp')
true
> {ownProp: true}.hasOwnProperty('abc')
falseDas Aufrufen von .hasOwnProperty() auf einem beliebigen Objekt kann auf zwei Arten fehlschlagen. Einerseits ist diese Methode nicht verfügbar, wenn ein Objekt keine Instanz von Object ist (siehe §29.7.3 „Nicht alle Objekte sind Instanzen von Object“).
const obj = Object.create(null);
assert.equal(obj instanceof Object, false);
assert.throws(
() => obj.hasOwnProperty('prop'),
{
name: 'TypeError',
message: 'obj.hasOwnProperty is not a function',
}
);Andererseits können wir .hasOwnProperty() nicht verwenden, wenn ein Objekt sie mit einer eigenen Eigenschaft überschreibt (Zeile A).
const obj = {
hasOwnProperty: 'yes' // (A)
};
assert.throws(
() => obj.hasOwnProperty('prop'),
{
name: 'TypeError',
message: 'obj.hasOwnProperty is not a function',
}
);Es gibt jedoch eine sichere Möglichkeit, .hasOwnProperty() zu verwenden.
function hasOwnProp(obj, propName) {
return Object.prototype.hasOwnProperty.call(obj, propName); // (A)
}
assert.equal(
hasOwnProp(Object.create(null), 'prop'), false
);
assert.equal(
hasOwnProp({hasOwnProperty: 'yes'}, 'prop'), false
);
assert.equal(
hasOwnProp({hasOwnProperty: 'yes'}, 'hasOwnProperty'), true
);Der Methodenaufruf in Zeile A wird in §29.3.5 „Dispatched vs. direkte Methodenaufrufe“ erklärt.
Wir können auch .bind() verwenden, um hasOwnProp() zu implementieren.
const hasOwnProp = Object.prototype.hasOwnProperty.call
.bind(Object.prototype.hasOwnProperty);Wie funktioniert das? Wenn wir .call() wie in Zeile A im vorherigen Beispiel aufrufen, tut es genau das, was hasOwnProp() tun soll, einschließlich der Vermeidung von Fallstricken. Wenn wir es jedoch als Funktionsaufruf verwenden möchten, können wir es nicht einfach extrahieren, wir müssen auch sicherstellen, dass sein this immer den richtigen Wert hat. Das ist es, was .bind() tut.
Ist es nie in Ordnung,
Object.prototype-Methoden über dynamische Dispatch zu verwenden?
In einigen Fällen können wir faul sein und Object.prototype-Methoden wie normale Methoden aufrufen (ohne .call() oder .bind()): Wenn wir die Receiver kennen und es sich um Objekte mit festem Layout handelt.
Wenn wir andererseits ihre Receiver nicht kennen und/oder es sich um Dictionary-Objekte handelt, müssen wir Vorsichtsmaßnahmen treffen.
Object.prototype.toString()Durch das Überschreiben von .toString() (in einer Unterklasse oder einer Instanz) können wir konfigurieren, wie Objekte in Strings umgewandelt werden.
> String({toString() { return 'Hello!' }})
'Hello!'
> String({})
'[object Object]'Für die Umwandlung von Objekten in Strings ist es besser, String() zu verwenden, da dies auch mit undefined und null funktioniert.
> undefined.toString()
TypeError: Cannot read properties of undefined (reading 'toString')
> null.toString()
TypeError: Cannot read properties of null (reading 'toString')
> String(undefined)
'undefined'
> String(null)
'null'Object.prototype.toLocaleString().toLocaleString() ist eine Version von .toString(), die über eine Locale und oft zusätzliche Optionen konfiguriert werden kann. Jede Klasse oder Instanz kann diese Methode implementieren. In der Standardbibliothek tun dies die folgenden Klassen:
Array.prototype.toLocaleString()Number.prototype.toLocaleString()Date.prototype.toLocaleString()TypedArray.prototype.toLocaleString()BigInt.prototype.toLocaleString()Als Beispiel wird hier gezeigt, wie Zahlen mit Dezimalbrüchen je nach Locale unterschiedlich in Strings umgewandelt werden ('fr' ist Französisch, 'en' ist Englisch).
> 123.45.toLocaleString('fr')
'123,45'
> 123.45.toLocaleString('en')
'123.45'Object.prototype.valueOf()Durch das Überschreiben von .valueOf() (in einer Unterklasse oder einer Instanz) können wir konfigurieren, wie Objekte in nicht-String-Werte (oft Zahlen) umgewandelt werden.
> Number({valueOf() { return 123 }})
123
> Number({})
NaNObject.prototype.isPrototypeOf()proto.isPrototypeOf(obj) gibt true zurück, wenn proto in der Prototypkette von obj vorhanden ist, und andernfalls false.
const a = {};
const b = {__proto__: a};
const c = {__proto__: b};
assert.equal(a.isPrototypeOf(b), true);
assert.equal(a.isPrototypeOf(c), true);
assert.equal(a.isPrototypeOf(a), false);
assert.equal(c.isPrototypeOf(a), false);So verwenden Sie diese Methode sicher (Details siehe §29.8.1 „Sichere Verwendung von Object.prototype-Methoden“).
const obj = {
// Overrides Object.prototype.isPrototypeOf
isPrototypeOf: true,
};
// Doesn’t work in this case:
assert.throws(
() => obj.isPrototypeOf(Object.prototype),
{
name: 'TypeError',
message: 'obj.isPrototypeOf is not a function',
}
);
// Safe way of using .isPrototypeOf():
assert.equal(
Object.prototype.isPrototypeOf.call(obj, Object.prototype), false
);Object.prototype.propertyIsEnumerable()obj.propertyIsEnumerable(propKey) gibt true zurück, wenn obj eine eigene aufzählbare Eigenschaft mit dem Schlüssel propKey hat, und andernfalls false.
const proto = {
enumerableProtoProp: true,
};
const obj = {
__proto__: proto,
enumerableObjProp: true,
nonEnumObjProp: true,
};
Object.defineProperty(
obj, 'nonEnumObjProp',
{
enumerable: false,
}
);
assert.equal(
obj.propertyIsEnumerable('enumerableProtoProp'),
false // not an own property
);
assert.equal(
obj.propertyIsEnumerable('enumerableObjProp'),
true
);
assert.equal(
obj.propertyIsEnumerable('nonEnumObjProp'),
false // not enumerable
);
assert.equal(
obj.propertyIsEnumerable('unknownProp'),
false // not a property
);So verwenden Sie diese Methode sicher (Details siehe §29.8.1 „Sichere Verwendung von Object.prototype-Methoden“).
const obj = {
// Overrides Object.prototype.propertyIsEnumerable
propertyIsEnumerable: true,
enumerableProp: 'yes',
};
// Doesn’t work in this case:
assert.throws(
() => obj.propertyIsEnumerable('enumerableProp'),
{
name: 'TypeError',
message: 'obj.propertyIsEnumerable is not a function',
}
);
// Safe way of using .propertyIsEnumerable():
assert.equal(
Object.prototype.propertyIsEnumerable.call(obj, 'enumerableProp'),
true
);Eine weitere sichere Alternative sind Property-Deskriptoren.
assert.deepEqual(
Object.getOwnPropertyDescriptor(obj, 'enumerableProp'),
{
value: 'yes',
writable: true,
enumerable: true,
configurable: true,
}
);Object.prototype.__proto__ (Accessor)Die Eigenschaft __proto__ existiert in zwei Versionen:
Object haben.Ich empfehle, die erstere Funktion zu vermeiden.
Object.prototype-Methoden“ erklärt, funktioniert sie nicht mit allen Objekten.Im Gegensatz dazu funktioniert __proto__ in Objekt-Literalen immer und ist nicht veraltet.
Lesen Sie weiter, wenn Sie daran interessiert sind, wie der Accessor __proto__ funktioniert.
__proto__ ist ein Accessor von Object.prototype, der von allen Instanzen von Object geerbt wird. Die Implementierung über eine Klasse würde so aussehen:
class Object {
get __proto__() {
return Object.getPrototypeOf(this);
}
set __proto__(other) {
Object.setPrototypeOf(this, other);
}
// ···
}Da __proto__ von Object.prototype geerbt wird, können wir dieses Feature entfernen, indem wir ein Objekt erstellen, das Object.prototype nicht in seiner Prototypkette hat (siehe §29.7.3 „Nicht alle Objekte sind Instanzen von Object“).
> '__proto__' in {}
true
> '__proto__' in Object.create(null)
falseObject.prototype.hasOwnProperty() Bessere Alternative zu
.hasOwnProperty(): Object.hasOwn() [ES2022]
Siehe §28.9.4 „Object.hasOwn(): Ist eine gegebene Eigenschaft eine eigene (nicht geerbte)? [ES2022]“.
obj.hasOwnProperty(propKey) gibt true zurück, wenn obj eine eigene (nicht geerbte) Eigenschaft mit dem Schlüssel propKey hat, und andernfalls false.
const obj = { ownProp: true };
assert.equal(
obj.hasOwnProperty('ownProp'), true // own
);
assert.equal(
'toString' in obj, true // inherited
);
assert.equal(
obj.hasOwnProperty('toString'), false
);So verwenden Sie diese Methode sicher (Details siehe §29.8.1 „Sichere Verwendung von Object.prototype-Methoden“).
const obj = {
// Overrides Object.prototype.hasOwnProperty
hasOwnProperty: true,
};
// Doesn’t work in this case:
assert.throws(
() => obj.hasOwnProperty('anyPropKey'),
{
name: 'TypeError',
message: 'obj.hasOwnProperty is not a function',
}
);
// Safe way of using .hasOwnProperty():
assert.equal(
Object.prototype.hasOwnProperty.call(obj, 'anyPropKey'), false
);Dies geschieht, um hervorzuheben, wie unterschiedlich Eigenschaften (öffentliche Slots) und private Slots sind: Durch Ändern der Reihenfolge der Adjektive werden die Wörter „öffentlich“ und „Feld“ sowie die Wörter „privat“ und „Feld“ immer zusammen genannt.
#? Warum private Felder nicht über private deklarieren?Könnten private Felder über private deklariert und normale Bezeichner verwendet werden? Betrachten wir, was passieren würde, wenn das möglich wäre:
class MyClass {
private value; // (A)
compare(other) {
return this.value === other.value;
}
}Immer wenn ein Ausdruck wie other.value im Body von MyClass erscheint, muss JavaScript entscheiden:
.value eine Eigenschaft?.value ein privates Feld?Zur Kompilierzeit weiß JavaScript nicht, ob die Deklaration in Zeile A für other gilt (weil es eine Instanz von MyClass ist) oder nicht. Das lässt zwei Optionen für die Entscheidungsfindung:
.value wird immer als privates Feld interpretiert.other eine Instanz von MyClass ist, wird .value als privates Feld interpretiert..value als Eigenschaft interpretiert.Beide Optionen haben Nachteile.
.value nicht mehr als Eigenschaft verwenden – für kein Objekt.Deshalb wurde das Namenspräfix # eingeführt. Die Entscheidung ist nun einfach: Wenn wir # verwenden, wollen wir auf ein privates Feld zugreifen. Wenn wir es nicht tun, wollen wir auf eine Eigenschaft zugreifen.
private funktioniert in statisch typisierten Sprachen (wie TypeScript), weil sie zur Kompilierzeit wissen, ob other eine Instanz von MyClass ist, und .value dann als privat oder öffentlich behandeln können.
Quiz
Siehe Quiz-App.