static# (private)get (Getter) und set (Setter)* (Generator)asyncIn diesem Kapitel untersuchen wir, wie Klassendefinitionen in TypeScript funktionieren.
Dieser Abschnitt ist ein Spickzettel für Klassendefinitionen in reinem JavaScript.
class OtherClass {}
class MyClass1 extends OtherClass {
publicInstanceField = 1;
constructor() {
super();
}
publicPrototypeMethod() {
return 2;
}
}
const inst1 = new MyClass1();
assert.equal(inst1.publicInstanceField, 1);
assert.equal(inst1.publicPrototypeMethod(), 2); Die folgenden Abschnitte handeln von Modifikatoren
Am Ende gibt es eine Tabelle, die zeigt, wie Modifikatoren kombiniert werden können.
staticclass MyClass2 {
static staticPublicField = 1;
static staticPublicMethod() {
return 2;
}
}
assert.equal(MyClass2.staticPublicField, 1);
assert.equal(MyClass2.staticPublicMethod(), 2);# (private)class MyClass3 {
#privateField = 1;
#privateMethod() {
return 2;
}
static accessPrivateMembers() {
// Private members can only be accessed from inside class definitions
const inst3 = new MyClass3();
assert.equal(inst3.#privateField, 1);
assert.equal(inst3.#privateMethod(), 2);
}
}
MyClass3.accessPrivateMembers();Warnung für JavaScript
TypeScript unterstützt private Felder seit Version 3.8, aber derzeit keine privaten Methoden.
get (Getter) und set (Setter)Accessoren sind im Grunde Methoden, die durch den Zugriff auf Eigenschaften aufgerufen werden. Es gibt zwei Arten von Accessoren: Getter und Setter.
class MyClass5 {
#name = 'Rumpelstiltskin';
/** Prototype getter */
get name() {
return this.#name;
}
/** Prototype setter */
set name(value) {
this.#name = value;
}
}
const inst5 = new MyClass5();
assert.equal(inst5.name, 'Rumpelstiltskin'); // getter
inst5.name = 'Queen'; // setter
assert.equal(inst5.name, 'Queen'); // getter* (Generator)class MyClass6 {
* publicPrototypeGeneratorMethod() {
yield 'hello';
yield 'world';
}
}
const inst6 = new MyClass6();
assert.deepEqual(
[...inst6.publicPrototypeGeneratorMethod()],
['hello', 'world']);asyncclass MyClass7 {
async publicPrototypeAsyncMethod() {
const result = await Promise.resolve('abc');
return result + result;
}
}
const inst7 = new MyClass7();
inst7.publicPrototypeAsyncMethod()
.then(result => assert.equal(result, 'abcabc'));const publicInstanceFieldKey = Symbol('publicInstanceFieldKey');
const publicPrototypeMethodKey = Symbol('publicPrototypeMethodKey');
class MyClass8 {
[publicInstanceFieldKey] = 1;
[publicPrototypeMethodKey]() {
return 2;
}
}
const inst8 = new MyClass8();
assert.equal(inst8[publicInstanceFieldKey], 1);
assert.equal(inst8[publicPrototypeMethodKey](), 2);Kommentare
Symbol.iterator. Aber jeder Ausdruck kann innerhalb der eckigen Klammern verwendet werden.Felder (kein Level bedeutet, dass ein Konstrukt auf Instanzebene existiert)
| Level | Sichtbarkeit |
|---|---|
| (Instanz) | |
| (Instanz) | # |
Statisch |
|
Statisch |
# |
Methoden (kein Level bedeutet, dass ein Konstrukt auf Prototyp-Ebene existiert)
| Level | Accessor | Async | Generator | Sichtbarkeit |
|---|---|---|---|---|
| (Prototyp) | ||||
| (Prototyp) | get |
|||
| (Prototyp) | set |
|||
| (Prototyp) | async |
|||
| (Prototyp) | * |
|||
| (Prototyp) | async |
* |
||
| (Prototyp-bezogen) | # |
|||
| (Prototyp-bezogen) | get |
# |
||
| (Prototyp-bezogen) | set |
# |
||
| (Prototyp-bezogen) | async |
# |
||
| (Prototyp-bezogen) | * |
# |
||
| (Prototyp-bezogen) | async |
* |
# |
|
Statisch |
||||
Statisch |
get |
|||
Statisch |
set |
|||
Statisch |
async |
|||
Statisch |
* |
|||
Statisch |
async |
* |
||
Statisch |
# |
|||
Statisch |
get |
# |
||
Statisch |
set |
# |
||
Statisch |
async |
# |
||
Statisch |
* |
# |
||
Statisch |
async |
* |
# |
Einschränkungen von Methoden
async noch Generatoren sein.Es ist wichtig zu bedenken, dass bei Klassen zwei Ketten von Prototypobjekten existieren.
Betrachten wir das folgende reine JavaScript-Beispiel:
class ClassA {
static staticMthdA() {}
constructor(instPropA) {
this.instPropA = instPropA;
}
prototypeMthdA() {}
}
class ClassB extends ClassA {
static staticMthdB() {}
constructor(instPropA, instPropB) {
super(instPropA);
this.instPropB = instPropB;
}
prototypeMthdB() {}
}
const instB = new ClassB(0, 1);Abb. 1 1 zeigt, wie die Prototypketten aussehen, die von ClassA und ClassB erstellt werden.
Standardmäßig sind alle Datenslots in TypeScript öffentliche Eigenschaften. Es gibt zwei Möglichkeiten, Daten privat zu halten:
Wir betrachten beide als Nächstes.
Beachten Sie, dass TypeScript derzeit keine privaten Methoden unterstützt.
Private Eigenschaften sind ein (statisches) Feature nur in TypeScript. Jede Eigenschaft kann privat gemacht werden, indem ihr das Schlüsselwort private vorangestellt wird (Zeile A).
class PersonPrivateProperty {
private name: string; // (A)
constructor(name: string) {
this.name = name;
}
sayHello() {
return `Hello ${this.name}!`;
}
}Wir erhalten nun Kompilierungsfehler, wenn wir auf diese Eigenschaft im falschen Geltungsbereich zugreifen (Zeile A).
const john = new PersonPrivateProperty('John');
assert.equal(
john.sayHello(), 'Hello John!');
// @ts-expect-error: Property 'name' is private and only accessible
// within class 'PersonPrivateProperty'. (2341)
john.name; // (A)private ändert jedoch nichts zur Laufzeit. Dort ist die Eigenschaft .name von einer öffentlichen Eigenschaft nicht zu unterscheiden.
assert.deepEqual(
Object.keys(john),
['name']);Wir können auch sehen, dass private Eigenschaften zur Laufzeit nicht geschützt sind, wenn wir uns den JavaScript-Code ansehen, zu dem die Klasse kompiliert wird.
class PersonPrivateProperty {
constructor(name) {
this.name = name;
}
sayHello() {
return `Hello ${this.name}!`;
}
}Private Felder sind ein neues JavaScript-Feature, das TypeScript seit Version 3.8 unterstützt.
class PersonPrivateField {
#name: string;
constructor(name: string) {
this.#name = name;
}
sayHello() {
return `Hello ${this.#name}!`;
}
}Diese Version von Person wird weitgehend genauso verwendet wie die Version mit privaten Eigenschaften.
const john = new PersonPrivateField('John');
assert.equal(
john.sayHello(), 'Hello John!');Dieses Mal ist die Eingekapselung jedoch vollständig. Die Verwendung der privaten Feld-Syntax außerhalb von Klassen ist sogar ein JavaScript-Syntaxfehler. Deshalb müssen wir eval() in Zeile A verwenden, damit wir diesen Code ausführen können.
assert.throws(
() => eval('john.#name'), // (A)
{
name: 'SyntaxError',
message: "Private field '#name' must be declared in "
+ "an enclosing class",
});
assert.deepEqual(
Object.keys(john),
[]);Das Kompilierungsergebnis ist nun viel komplizierter (leicht vereinfacht).
var __classPrivateFieldSet = function (receiver, privateMap, value) {
if (!privateMap.has(receiver)) {
throw new TypeError(
'attempted to set private field on non-instance');
}
privateMap.set(receiver, value);
return value;
};
// Omitted: __classPrivateFieldGet
var _name = new WeakMap();
class Person {
constructor(name) {
// Add an entry for this instance to _name
_name.set(this, void 0);
// Now we can use the helper function:
__classPrivateFieldSet(this, _name, name);
}
// ···
}Dieser Code verwendet eine gängige Technik, um Instanzdaten privat zu halten.
Weitere Informationen zu diesem Thema finden Sie unter „JavaScript for impatient programmers“.
Private Felder und private Eigenschaften können nicht in Unterklassen aufgerufen werden (Zeile A).
class PrivatePerson {
private name: string;
constructor(name: string) {
this.name = name;
}
sayHello() {
return `Hello ${this.name}!`;
}
}
class PrivateEmployee extends PrivatePerson {
private company: string;
constructor(name: string, company: string) {
super(name);
this.company = company;
}
sayHello() {
// @ts-expect-error: Property 'name' is private and only
// accessible within class 'PrivatePerson'. (2341)
return `Hello ${this.name} from ${this.company}!`; // (A)
}
}Wir können das vorherige Beispiel korrigieren, indem wir in Zeile A von private auf protected umstellen (wir stellen auch in Zeile B um, der Konsistenz halber).
class ProtectedPerson {
protected name: string; // (A)
constructor(name: string) {
this.name = name;
}
sayHello() {
return `Hello ${this.name}!`;
}
}
class ProtectedEmployee extends ProtectedPerson {
protected company: string; // (B)
constructor(name: string, company: string) {
super(name);
this.company = company;
}
sayHello() {
return `Hello ${this.name} from ${this.company}!`; // OK
}
}Konstruktoren können ebenfalls privat sein. Das ist nützlich, wenn wir statische Factory-Methoden haben und möchten, dass Kunden immer diese Methoden verwenden und niemals den Konstruktor direkt aufrufen. Statische Methoden können private Klassenmember aufrufen, weshalb die Factory-Methoden den Konstruktor weiterhin verwenden können.
Im folgenden Code gibt es eine statische Factory-Methode DataContainer.create(). Sie richtet Instanzen über asynchron geladene Daten ein. Die asynchronen Codezeilen in der Factory-Methode zu halten, ermöglicht es der eigentlichen Klasse, vollständig synchron zu sein.
class DataContainer {
#data: string;
static async create() {
const data = await Promise.resolve('downloaded'); // (A)
return new this(data);
}
private constructor(data: string) {
this.#data = data;
}
getData() {
return 'DATA: '+this.#data;
}
}
DataContainer.create()
.then(dc => assert.equal(
dc.getData(), 'DATA: downloaded'));In realem Code würden wir fetch() oder eine ähnliche Promise-basierte API verwenden, um Daten asynchron in Zeile A zu laden.
Der private Konstruktor verhindert, dass DataContainer unterklassenfähig ist. Wenn wir Unterklassen zulassen wollen, müssen wir ihn auf protected setzen.
Wenn die Compiler-Einstellung --strictPropertyInitialization aktiviert ist (was der Fall ist, wenn wir --strict verwenden), prüft TypeScript, ob alle deklarierten Instanzeigenschaften korrekt initialisiert werden.
Entweder durch Zuweisungen im Konstruktor
class Point {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}Oder durch Initialisierer für die Eigenschaftsdeklarationen.
class Point {
x = 0;
y = 0;
// No constructor needed
}Manchmal initialisieren wir Eigenschaften jedoch auf eine Weise, die TypeScript nicht erkennt. Dann können wir Ausrufezeichen (definite assignment assertions) verwenden, um die Warnungen von TypeScript abzuschalten (Zeile A und Zeile B).
class Point {
x!: number; // (A)
y!: number; // (B)
constructor() {
this.initProperties();
}
initProperties() {
this.x = 0;
this.y = 0;
}
}Im folgenden Beispiel benötigen wir ebenfalls „definite assignment assertions“. Hier richten wir Instanzeigenschaften über den Konstruktorparameter props ein.
class CompilerError implements CompilerErrorProps { // (A)
line!: number;
description!: string;
constructor(props: CompilerErrorProps) {
Object.assign(this, props); // (B)
}
}
// Helper interface for the parameter properties
interface CompilerErrorProps {
line: number,
description: string,
}
// Using the class:
const err = new CompilerError({
line: 123,
description: 'Unexpected token',
});Hinweise
Object.assign(), um die Eigenschaften des Parameters props in this zu kopieren.implements sicher, dass die Klasse alle Eigenschaften deklariert, die Teil des Interfaces CompilerErrorProps sind.public, private oder protected machenWenn wir das Schlüsselwort public für einen Konstruktorparameter verwenden, dann tut TypeScript zwei Dinge für uns:
Daher sind die folgenden beiden Klassen äquivalent:
class Point1 {
constructor(public x: number, public y: number) {
}
}
class Point2 {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}Wenn wir anstelle von public private oder protected verwenden, sind die entsprechenden Instanzeigenschaften privat oder geschützt (nicht öffentlich).
Zwei Konstrukte können in TypeScript abstrakt sein:
Der folgende Code demonstriert abstrakte Klassen und Methoden.
Einerseits gibt es die abstrakte Oberklasse Printable und ihre Hilfsklasse StringBuilder.
class StringBuilder {
string = '';
add(str: string) {
this.string += str;
}
}
abstract class Printable {
toString() {
const out = new StringBuilder();
this.print(out);
return out.string;
}
abstract print(out: StringBuilder): void;
}Andererseits gibt es die konkreten Unterklassen Entries und Entry.
class Entries extends Printable {
entries: Entry[];
constructor(entries: Entry[]) {
super();
this.entries = entries;
}
print(out: StringBuilder): void {
for (const entry of this.entries) {
entry.print(out);
}
}
}
class Entry extends Printable {
key: string;
value: string;
constructor(key: string, value: string) {
super();
this.key = key;
this.value = value;
}
print(out: StringBuilder): void {
out.add(this.key);
out.add(': ');
out.add(this.value);
out.add('\n');
}
}Und schließlich verwenden wir Entries und Entry.
const entries = new Entries([
new Entry('accept-ranges', 'bytes'),
new Entry('content-length', '6518'),
]);
assert.equal(
entries.toString(),
'accept-ranges: bytes\ncontent-length: 6518\n');Hinweise zu abstrakten Klassen