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.
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)));undefinedfalseget 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
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}`);MariatrueSeat(5, 12)Reihe 5, Platz 12Object.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 eineMap, undgetfindet 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
- Eine leere Map: Der Schlüssel ist der Kinoplatz als String, der Wert ist ein Name.
- set legt den Schlüssel "5-12" an, gebildet aus Seat(5, 12), mit dem Wert „Maria“.
- get bekommt den Schlüssel eines neuen Seat-Objekts. Das Objekt ist ein anderes, nicht das aus set.
- key() bildet den String aus den Feldern, deshalb ist er gleich: "5-12".
- 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.
- has macht dasselbe mit dem Schlüssel eines weiteren neuen Seat-Objekts: true.
- 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.rowbleibt 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);5[ 'Kaffee', 'Honig' ]- Im Strict Mode bricht dieselbe Änderung mit einem
TypeErrorab. 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 mitimport:
"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}`);}TypeError: Cannot assign to read only property 'row' of object '[object Object]'Die Idee
Prüfungen und Methoden
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);}Price(18.5 EUR)Price(14.8 EUR)Der Preis darf nicht negativ sein: -5RangeErrorist 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 neuenPricezurück. So arbeitest du mit Objekten, die sich nicht ändern:coffeebleibt 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);a und b sind zwei Objekte, deshalb ist === false: Es fragt, ob es dasselbe Objekt ist. Die Schlüssel sind Strings aus denselben Feldern, und die sind gleich. Ein Set vergleicht Objekte wie eine Map danach, ob es dasselbe Objekt ist, deshalb kommen beide hinein: Die Größe ist 2.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.
price = 4.5; steht kein break. Der switch in JavaScript hält am Ende des Falls nicht an, sondern fällt durch in den nächsten: Er führt auch price = 6.9; aus und kommt erst dann zum break. Die Lösung ist ein break nach Zeile 5. JavaScript hat keinen switch, der einen Wert zurückgibt, aber auch return beendet den Fall. Das ist die nächste Karte.Die Idee
switch mit return
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} €`);Lieferung: 4.5 €returnverlässt die Funktion, deshalb gibt es danach kein Durchfallen, und du schreibst keinbreak.- Zwei Fälle mit demselben Code stehen untereinander:
case "kurier":ist leer und fällt incase "adresse":durch. Das ist das nützliche Durchfallen. defaultfängt den Rest. Hier wirft es eine Exception.- JavaScript prüft nicht, dass jede Eingabe abgedeckt ist. Ohne
defaultpasst kein Fall auf eine unbekannte Lieferart: Die Funktion kommt an ihr Ende und gibtundefinedzurück, ohne Fehler:
function deliveryPrice(method) { switch (method) { case "filiale": return 4.5; case "kurier": return 6.9; }}
console.log(deliveryPrice("post"));undefinedWenn 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));6.99.9Genauer, 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");6.9unbekannte LieferartDie 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
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));30.52Das 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}`);}TypeError: Unbekannte Zahlung: VoucherDie 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()));Karte ****4444Überweisung von einem deutschen KontoÜberweisung aus dem AuslandNachnahmeGenauer, 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));DEDEProbier es aus
Welcher Fall 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));amount > 500 und landen bei der zweiten. Die Kartenzahlung beträgt 650 €, und eine Bestätigung braucht es erst über 1000 €, also gewinnt die Prüfung ohne Bedingung an den Betrag: „Karte“.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));NachnahmeDie 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 EckladenKaffeebohnen 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);Der EckladenKaffeebohnen 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, undtoFixed(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} €`);}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.Kreis: 3.14Rechteck: 6.00Quadrat: 16.00Dreieck: 7.50Gesamt: 32.64ToOffice 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.ToOffice(Hamburg Zentrum) -> 4.50 €ToAddress(Berlin, 2) -> 5.90 €ToAddress(Hamburg, 8) -> 9.90 €Pickup() -> 0.00 €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.(2, 3): 3(5, 1): 2(0, 0): 1Besuche bei (2, 3): 3Weitere Aufgaben
- Ein Schüler als Datenklasse.
Studentmitnameundgrade, dessen Konstruktor für eine Note außerhalb von 2 bis 6 einenRangeErrorwirft und das Objekt einfriert. Sortiere ein Array von Schülern mittoSorted((a, b) => a.grade - b.grade). - Wochentag. Schreib die Aufgabe „Wochentag“ aus der Lektion „Bedingungen“ als Funktion mit
switchundreturnneu: die Fälle von 1 bis 5 untereinander, dannreturn "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
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Map und Set · Hashcodes und Hashfunktionen · Pfeilfunktionen · Array-Methoden
- Geht weiter in: Entwurfsmuster · Datenbanken · Observer · Factory Method und Simple Factory
- Aufgaben zum Thema: Modernes JavaScript