Zum Inhalt
wannacode

Daten, switch und Destrukturierung

Kurz gesagt: JavaScript hat kein record: Eine Datenklasse baut man mit Object.freeze, und switch mit return, instanceof und Destrukturierung ersetzen die Muster.

Problem

Der Platz ist belegt, aber die Map findet ihn nicht

Reservierungen im Kino: Der Schlüssel ist der Platz (Reihe und Nummer), der Wert ist der Name der Person.

Seat
class Seat {
constructor(row, number) {
this.row = row;
this.number = number;
}
}
const reservations = new Map();
reservations.set(new Seat(5, 12), "Maria");
console.log(reservations.get(new Seat(5, 12)));
console.log(reservations.has(new Seat(5, 12)));
Ausgabe
undefined
false
Maria sitzt in Reihe 5, Platz 12, aber get gibt undefined zurück. new Seat(5, 12) zweimal geschrieben sind zwei verschiedene Objekte, und eine Map vergleicht Objekte als Schlüssel danach, ob es dasselbe Objekt ist. Methoden wie equals und hashCode in Java, mit denen eine Klasse sagt, wann zwei Objekte gleich sind, hat JavaScript nicht.

Die Idee

Eine Klasse für Daten und ein Schlüssel aus den Feldern

JavaScript hat kein record. Was Java selbst schreibt, schreibst du hier selbst:
class Seat {
constructor(row, number) {
this.row = row;
this.number = number;
Object.freeze(this);
}
key() {
return `${this.row}-${this.number}`;
}
toString() {
return `Seat(${this.row}, ${this.number})`;
}
}
const reservations = new Map();
reservations.set(new Seat(5, 12).key(), "Maria");
console.log(reservations.get(new Seat(5, 12).key()));
console.log(reservations.has(new Seat(5, 12).key()));
const seat = new Seat(5, 12);
console.log(`${seat}`);
console.log(`Reihe ${seat.row}, Platz ${seat.number}`);
Ausgabe
Maria
true
Seat(5, 12)
Reihe 5, Platz 12
  • Object.freeze(this) am Ende des Konstruktors verbietet, die Felder zu ändern.
  • key() macht aus den Feldern einen String: "5-12". Zwei Strings mit denselben Zeichen sind gleich, deshalb ist der String ein guter Schlüssel für eine Map, und get findet Maria jetzt.
  • toString() sagt, wie das Objekt als Text aussieht.

Die Felder liest du direkt: seat.row.

const reservations = new Map();
reservations.set(new Seat(5, 12).key(), "Maria");
console.log(reservations.get(new Seat(5, 12).key()));
console.log(reservations.has(new Seat(5, 12).key()));
const seat = new Seat(5, 12);
console.log(`${seat}`);
console.log(`Reihe ${seat.row}, Platz ${seat.number}`);
Schritt für Schritt, in Worten
  1. Eine leere Map: Der Schlüssel ist der Kinoplatz als String, der Wert ist ein Name.
  2. set legt den Schlüssel "5-12" an, gebildet aus Seat(5, 12), mit dem Wert „Maria“.
  3. get bekommt den Schlüssel eines neuen Seat-Objekts. Das Objekt ist ein anderes, nicht das aus set.
  4. key() bildet den String aus den Feldern, deshalb ist er gleich: "5-12".
  5. Die beiden Strings sind aus gleichen Feldern gebildet: Reihe 5 und 5, Platz 12 und 12. Sie sind gleich, und get gibt „Maria“ zurück.
  6. has macht dasselbe mit dem Schlüssel eines weiteren neuen Seat-Objekts: true.
  7. toString ist hier von Hand geschrieben, und die Felder liest man direkt: seat.row.
Genauer, für Neugierige
  • Eine Änderung an einem eingefrorenen Feld geht nicht durch: seat.row bleibt 5. In einer normalen Datei gibt es keinen Fehler, und der Ausrutscher ist schwer zu bemerken. Auch die Unveränderlichkeit reicht nur eine Ebene tief: Ein Array in einem Feld kann sich ändern.
class Seat {
constructor(row, number) {
this.row = row;
this.number = number;
Object.freeze(this);
}
key() {
return `${this.row}-${this.number}`;
}
toString() {
return `Seat(${this.row}, ${this.number})`;
}
}
const seat = new Seat(5, 12);
seat.row = 6;
console.log(seat.row);
class Cart {
constructor(items) {
this.items = items;
Object.freeze(this);
}
}
const cart = new Cart(["Kaffee"]);
cart.items.push("Honig");
console.log(cart.items);
Ausgabe
5
[ 'Kaffee', 'Honig' ]
  • Im Strict Mode bricht dieselbe Änderung mit einem TypeError ab. Der Strict Mode gilt für den Code in einer Klasse, für eine Datei, die mit "use strict"; beginnt, und für ein Modul mit import:
"use strict";
class Seat {
constructor(row, number) {
this.row = row;
this.number = number;
Object.freeze(this);
}
key() {
return `${this.row}-${this.number}`;
}
toString() {
return `Seat(${this.row}, ${this.number})`;
}
}
const seat = new Seat(5, 12);
try {
seat.row = 6;
} catch (e) {
console.log(`${e.name}: ${e.message}`);
}
Ausgabe
TypeError: Cannot assign to read only property 'row' of object '[object Object]'

Die Idee

Prüfungen und Methoden

Ein Preis kann nicht negativ sein. Die Prüfung steht am Anfang des Konstruktors, bevor die Felder gesetzt werden.
class Price {
constructor(amount, currency) {
if (amount < 0) {
throw new RangeError(`Der Preis darf nicht negativ sein: ${amount}`);
}
this.amount = amount;
this.currency = currency;
Object.freeze(this);
}
withDiscount(percent) {
return new Price(this.amount * (1 - percent / 100), this.currency);
}
toString() {
return `Price(${this.amount} ${this.currency})`;
}
}
const coffee = new Price(18.5, "EUR");
const sale = coffee.withDiscount(20);
console.log(`${coffee}`);
console.log(`${sale}`);
try {
new Price(-5, "EUR");
} catch (e) {
console.log(e.message);
}
Ausgabe
Price(18.5 EUR)
Price(14.8 EUR)
Der Preis darf nicht negativ sein: -5
  • RangeError ist die eingebaute Fehlerart für einen Wert außerhalb des erlaubten Bereichs. Wenn der Konstruktor wirft, wird das Objekt nicht erzeugt.
  • withDiscount ändert den Preis nicht. Es gibt einen neuen Price zurück. So arbeitest du mit Objekten, die sich nicht ändern: coffee bleibt bei 18,50 €.

Probier es aus

Sind sie gleich?

Seat ist die Klasse mit key() aus der Karte davor.

Was gibt das Programm aus?

const a = new Seat(5, 12);
const b = new Seat(5, 12);
console.log(a === b);
console.log(a.key() === b.key());
console.log(new Set([a, b]).size);
Antwort

Probier es aus

Die Lieferung wird zu teuer

Die Lieferung in eine Filiale kostet 4,50 €, aber das Programm sagt 6,90 €. In welcher Zeile ist der Fehler?

Wähle die Zeile mit dem Fehler.

Die Idee

switch mit return

JavaScript hat keinen switch-Ausdruck, der einen Wert liefert. Deshalb kommt der switch in eine Funktion, und jeder Fall endet mit return.
function deliveryPrice(method) {
switch (method) {
case "filiale":
return 4.5;
case "kurier":
case "adresse":
return 6.9;
case "laden":
return 0;
default:
throw new Error(`Unbekannte Lieferart: ${method}`);
}
}
const price = deliveryPrice("filiale");
console.log(`Lieferung: ${price} €`);
Ausgabe
Lieferung: 4.5 €
  • return verlässt die Funktion, deshalb gibt es danach kein Durchfallen, und du schreibst kein break.
  • Zwei Fälle mit demselben Code stehen untereinander: case "kurier": ist leer und fällt in case "adresse": durch. Das ist das nützliche Durchfallen.
  • default fängt den Rest. Hier wirft es eine Exception.
  • JavaScript prüft nicht, dass jede Eingabe abgedeckt ist. Ohne default passt kein Fall auf eine unbekannte Lieferart: Die Funktion kommt an ihr Ende und gibt undefined zurück, ohne Fehler:
function deliveryPrice(method) {
switch (method) {
case "filiale":
return 4.5;
case "kurier":
return 6.9;
}
}
console.log(deliveryPrice("post"));
Ausgabe
undefined

Wenn ein Fall mehrere Zeilen und ein eigenes const hat, kommt er in { }, damit sie sich nicht mit den anderen Fällen vermischen:

function delivery(method, weightKg) {
switch (method) {
case "filiale":
return 4.5;
case "kurier": {
const base = 6.9;
return weightKg > 10 ? base + 3 : base;
}
default:
return 0;
}
}
console.log(delivery("kurier", 4));
console.log(delivery("kurier", 12));
Ausgabe
6.9
9.9
Genauer, für Neugierige

Wenn jeder Fall nur ein Wert ist, kannst du statt eines switch ein Objekt als Nachschlagetabelle nehmen. ?? liefert den Ersatzwert für einen Schlüssel, den es nicht gibt:

const PRICES = { "filiale": 4.5, "kurier": 6.9, "laden": 0 };
console.log(PRICES["kurier"]);
console.log(PRICES["post"] ?? "unbekannte Lieferart");
Ausgabe
6.9
unbekannte Lieferart

Die Idee

Eine Familie von Typen

Der Shop nimmt drei Zahlungsarten an: Karte, Banküberweisung und Nachnahme. Andere gibt es nicht. Jede Art ist eine eigene Klasse:

class CardPayment {
constructor(cardNumber) {
this.cardNumber = cardNumber;
}
}
class BankTransfer {
constructor(iban) {
this.iban = iban;
}
}
class CashOnDelivery {}

Jede Zahlungsart ist nur Daten: eine Kartennummer, eine IBAN oder nichts. CashOnDelivery hat keine Felder: Sein Rumpf ist leer.

In Java sorgt sealed dafür, dass der Compiler einen vierten Typ ablehnt. JavaScript kann nicht sagen „nur diese drei“. Eine vierte Klasse kann überall auftauchen. Deshalb muss sich die Funktion, die die Arten unterscheidet, vor einer unbekannten Art schützen. Das ist die nächste Karte.

Sieh es dir an

instanceof und Destrukturierung

Die Gebühr hängt von der Zahlungsart ab. JavaScript hat keine Typmuster wie die in einem Java-switch. Die Prüfung geschieht mit instanceof, eine nach der anderen:
function fee(payment, amount) {
if (payment instanceof CardPayment) {
return amount * 0.015;
}
if (payment instanceof BankTransfer) {
return 0.5;
}
if (payment instanceof CashOnDelivery) {
return 2;
}
throw new TypeError(`Unbekannte Zahlung: ${payment.constructor.name}`);
}
console.log(fee(new CardPayment("4111222233334444"), 200));
console.log(fee(new BankTransfer("DE89370400440532013000"), 200));
console.log(fee(new CashOnDelivery(), 200));
Ausgabe
3
0.5
2

Das throw am Ende ist die Absicherung: Wenn das Objekt keine der drei Arten ist, stoppt das Programm mit einer klaren Meldung, statt still undefined zurückzugeben:

class Voucher {}
try {
fee(new Voucher(), 200);
} catch (e) {
console.log(`${e.name}: ${e.message}`);
}
Ausgabe
TypeError: Unbekannte Zahlung: Voucher

Die Felder holst du mit Destrukturierung heraus: const { iban } = payment; nimmt das Feld iban des Objekts und legt es in eine Variable mit demselben Namen. Die Bedingung für ein deutsches Konto ist ein normales ? :.

function describe(payment) {
if (payment instanceof CardPayment) {
const { cardNumber } = payment;
return `Karte ****${cardNumber.slice(-4)}`;
}
if (payment instanceof BankTransfer) {
const { iban } = payment;
return iban.startsWith("DE") ? "Überweisung von einem deutschen Konto" : "Überweisung aus dem Ausland";
}
if (payment instanceof CashOnDelivery) {
return "Nachnahme";
}
throw new TypeError(`Unbekannte Zahlung: ${payment.constructor.name}`);
}
console.log(describe(new CardPayment("4111222233334444")));
console.log(describe(new BankTransfer("DE89370400440532013000")));
console.log(describe(new BankTransfer("AT611904300234573201")));
console.log(describe(new CashOnDelivery()));
Ausgabe
Karte ****4444
Überweisung von einem deutschen Konto
Überweisung aus dem Ausland
Nachnahme
Genauer, für Neugierige
  • Beim Destrukturieren kann die Variable einen anderen Namen haben: const { iban: account } = transfer;. Du kannst es auch direkt in den Parametern einer Funktion machen: ({ iban }) => ....
const transfer = new BankTransfer("DE89370400440532013000");
const { iban: account } = transfer;
console.log(account.slice(0, 2));
const country = ({ iban }) => iban.slice(0, 2);
console.log(country(transfer));
Ausgabe
DE
DE

Probier es aus

Welcher Fall gewinnt?

Die Prüfungen laufen von oben nach unten. Die erste, die wahr ist, gewinnt.

Was gibt das Programm aus?

function label(payment, amount) {
if (payment instanceof CashOnDelivery && amount > 500) {
return "Nachnahme: über 500 € nicht möglich";
}
if (payment instanceof CashOnDelivery) {
return "Nachnahme";
}
if (payment instanceof CardPayment && amount > 1000) {
return "Karte: Bestätigung nötig";
}
if (payment instanceof CardPayment) {
return "Karte";
}
return "Überweisung";
}
console.log(label(new CashOnDelivery(), 650));
console.log(label(new CashOnDelivery(), 80));
console.log(label(new CardPayment("4111222233334444"), 650));
Antwort
Genauer, für Neugierige

Die Prüfung mit Bedingung muss vor der allgemeinen für dieselbe Art stehen. Andersherum ist es kein Fehler: Die allgemeine Prüfung fängt alles, und die mit der Bedingung wird einfach nie erreicht:

function label(payment, amount) {
if (payment instanceof CashOnDelivery) {
return "Nachnahme";
}
if (payment instanceof CashOnDelivery && amount > 500) {
return "Nachnahme: über 500 € nicht möglich";
}
return "sonstiges";
}
console.log(label(new CashOnDelivery(), 650));
Ausgabe
Nachnahme

Die Idee

Text über mehrere Zeilen

Text über mehrere Zeilen, wie ein Kassenbon oder JSON, ist mit " und \n in jeder Zeile schwer zu lesen. Ein Template-Literal zwischen zwei Backticks (`) kann über mehrere Zeilen gehen:

const receipt = `Der Eckladen
Kaffeebohnen 18.50 €
Honig 12.00 €
Summe: 30.50 €`;
console.log(receipt);
const name = "Maria";
const total = 75.5;
const json = `{"customer": "${name}", "total": ${total.toFixed(2)}}`;
console.log(json);
Ausgabe
Der Eckladen
Kaffeebohnen 18.50 €
Honig 12.00 €
Summe: 30.50 €
{"customer": "Maria", "total": 75.50}
  • Der Text beginnt direkt nach dem öffnenden Backtick. Deshalb steht die erste Zeile in derselben Zeile.
  • Die Einrückung bleibt, wie sie geschrieben ist. Deshalb stehen die Zeilen hier ganz links.
  • Normale Anführungszeichen darin werden nicht maskiert.
  • ${...} füllt die Werte aus, und toFixed(2) liefert zwei Nachkommastellen.

In Java lässt var den Compiler den Typ der Variable ermitteln. In JavaScript ist var die alte Art, eine Variable anzulegen, und wird in diesem Kurs nicht verwendet. Auch bei const und let schreibst du keinen Typ: Der Typ gehört zum Wert, nicht zum Namen.

const prices = new Map([
["Kaffee", 18.5],
["Honig", 12],
]);
for (const [name, price] of prices) {
console.log(`${name}: ${price} €`);
}
Ausgabe
Kaffee: 18.5 €
Honig: 12 €

Übung

Daten und Fälle

Löse die Aufgaben in deinem Editor.

1. Formen. Drei Klassen: Circle mit radius, Rectangle mit width und height und Triangle mit base und height. Schreib area(shape) mit instanceof und Destrukturierung, und wirf für eine unbekannte Art einen TypeError. Schreib auch name(shape), das für ein Rechteck mit gleichen Seiten „Quadrat“ sagt. Gib einen Kreis mit Radius 1, ein Rechteck 2 mal 3, ein Quadrat 4 mal 4, ein Dreieck mit Grundseite 3 und Höhe 5 und die Gesamtfläche aus.
Ausgabe
Kreis: 3.14
Rechteck: 6.00
Quadrat: 16.00
Dreieck: 7.50
Gesamt: 32.64
2. Lieferpreis. Drei Klassen: ToOffice mit office, ToAddress mit city und weightKg und Pickup ohne Felder, jede mit ihrem eigenen toString(). Der Preis: in eine Filiale 4,50 €; an eine Adresse 5,90 € in Berlin und 6,90 € anderswo, plus 1 € für jedes Kilogramm über 5; Abholung im Laden 0 €. Gib jede Lieferung (mit ihrem toString()) und ihren Preis aus.
Ausgabe
ToOffice(Hamburg Zentrum) -> 4.50 €
ToAddress(Berlin, 2) -> 5.90 €
ToAddress(Hamburg, 8) -> 9.90 €
Pickup() -> 0.00 €
3. Ein Punkt als Schlüssel. Der Kurier fährt Punkte auf der Karte ab: (2, 3), (5, 1), (2, 3), (0, 0), (5, 1), (2, 3). Mach eine Klasse Point mit x, y und key(), das den String (2, 3) zurückgibt, und zähle in einer Map mit (visits.get(key) ?? 0) + 1, wie oft er an jedem Punkt war. Schlag am Ende den Schlüssel von new Point(2, 3) nach.
Ausgabe
(2, 3): 3
(5, 1): 2
(0, 0): 1
Besuche bei (2, 3): 3

Weitere Aufgaben

  • Ein Schüler als Datenklasse. Student mit name und grade, dessen Konstruktor für eine Note außerhalb von 2 bis 6 einen RangeError wirft und das Objekt einfriert. Sortiere ein Array von Schülern mit toSorted((a, b) => a.grade - b.grade).
  • Wochentag. Schreib die Aufgabe „Wochentag“ aus der Lektion „Bedingungen“ als Funktion mit switch und return neu: die Fälle von 1 bis 5 untereinander, dann return "Werktag";.
  • Ein Brief. Mach mit einem mehrzeiligen Template-Literal einen Brief, der eine Reservierung bestätigt: Name, Datum, Anzahl der Gäste und Preis in Euro.

Test

Teste dich

1. const m = new Map(); m.set(new Seat(5, 12), "Maria"); Was liefert m.get(new Seat(5, 12))?
2. Was macht Object.freeze(this) am Ende des Konstruktors?
3. Was passiert in einem switch, wenn ein case weder break noch return hat?
4. Was macht const { iban } = payment;?
5. JavaScript hat kein sealed. Wie schützt sich die Funktion, die die Zahlungsarten unterscheidet, vor einer unbekannten Art?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·