Strings
Kurz gesagt: Ein String in JavaScript ändert sich nicht: Jedes += macht einen neuen Text, deshalb setzt man viele Stücke mit einem Array und join zusammen, und localeCompare vergleicht in alphabetischer Reihenfolge.
Problem
Ein neuer String bei jedem +=
Du weißt schon, dass title.toUpperCase(); allein nichts bewirkt: Ein String ist unveränderlich, und die Methode gibt einen neuen String zurück. Das hat einen Preis, und in einer Schleife sieht man ihn.
Das Programm des Onlineshops baut den Tagesbericht Zeile für Zeile zusammen:
let report = "";for (let i = 1; i <= 3; i++) { report += `Auftrag ${i}: 12.50 €\n`;}process.stdout.write(report);Auftrag 1: 12.50 €Auftrag 2: 12.50 €Auftrag 3: 12.50 €process.stdout.write gibt den Text aus, ohne am Ende eine neue Zeile anzuhängen: Der Bericht endet schon mit \n.
Nach den Regeln der Sprache hängt jedes report += ... nicht an den alten String an. Es macht einen neuen String: den ganzen bisherigen Bericht plus die neue Zeile. Würde die Engine wirklich bei jedem Durchlauf alles kopieren, dann würde gegen Ende von 50.000 Bestellungen jeder Durchlauf über eine Million Zeichen kopieren. So passiert es in Java (etwa 2,5 Sekunden) und in Python (etwa 6,5 Sekunden).
In Node braucht dieselbe Schleife mit 50.000 Bestellungen etwa 8 Millisekunden. Wir haben es auf dem Rechner gemessen, auf dem wir den Code der Lektionen prüfen. V8, die Engine, die JavaScript ausführt, schummelt: Bei += kopiert sie nicht. Sie merkt sich die beiden Stücke nebeneinander und klebt sie auf einmal zusammen, wenn du den Text brauchst. Das ist ein Trick der Engine, keine Regel der Sprache. Der String ist trotzdem unveränderlich.
Die Idee
Ein Array von Stücken und join
JavaScript hat keinen String, der sich an Ort und Stelle ändert. Deshalb sammelst du die Stücke in einem Array, und das ändert sich an Ort und Stelle. Am Ende klebt join sie zu einem einzigen String zusammen und kopiert jedes Zeichen nur einmal:
const parts = [];for (let i = 1; i <= 3; i++) { parts.push(`Auftrag ${i}: 12.50 €`);}const report = parts.join("\n");console.log(report);Auftrag 1: 12.50 €Auftrag 2: 12.50 €Auftrag 3: 12.50 €const parts = []ist ein leeres Array.pushhängt ein Stück ans Ende, ohne die anderen anzufassen.parts.join("\n")macht einen String aus allen Stücken, mit dem Trennzeichen"\n"dazwischen. Zum Zusammenkleben ohne Trennzeichen schreibst duparts.join("").- Die neue Zeile zwischen den Stücken kommt vom Trennzeichen, deshalb haben die Stücke hier kein
\nam Ende.
In Node sind beide Wege schnell: Mit 50.000 Bestellungen braucht join etwa 5 Millisekunden und += etwa 8. Ein Array und join sind der übliche Weg, wenn es viele Stücke gibt oder ein Trennzeichen dazwischen nötig ist, und sie hängen nicht vom Trick der Engine ab.
Das sagen die Regeln der Sprache zu beiden Wegen. Bei += schiebt V8 das Kopieren auf, aber der neue String ist trotzdem ein neuer String:
let report = "";for (let i = 1; i <= 3; i++) { report += `Auftrag ${i}: 12.50 €\n`;}const parts = [];for (let i = 1; i <= 3; i++) { parts.push(`Auftrag ${i}: 12.50 €\n`);}const joined = parts.join("");console.log(report.length, joined.length);console.log(report === joined);Schritt für Schritt, in Worten
- report zeigt auf einen leeren String.
- Das erste += macht einen neuen String mit einer Zeile: 19 Zeichen.
- Nach den Regeln der Sprache ergibt das zweite += noch einen String: die alte Zeile (19 Zeichen) und die neue. Den alten String braucht niemand mehr.
- Das dritte ergibt einen String mit 38 alten Zeichen und der neuen Zeile. Java und Python kopieren sie jedes Mal. V8 schiebt das Kopieren auf: Es merkt sich die Stücke und klebt sie einmal zusammen.
- parts ist ein leeres Array: Darin landen die Stücke.
- push legt jedes Stück direkt in das Array. Die alten Stücke werden nicht kopiert, das Array bleibt dasselbe.
- join klebt die Stücke einmal zusammen: 57 Zeichen. Der Text ist derselbe. Nach den Regeln kopiert += zusätzlich 57 alte Zeichen, join kein einziges.
Genauer, für Neugierige
- V8 nennt die zusammengeklebten Stücke ein „Seil“ (rope). Wenn du ein Zeichen aus so einem String liest, klebt die Engine ihn einmal zu einem einzigen String zusammen.
- Ein einzelnes Template-Literal wie
`Auftrag ${i}: ${price}`ist ein einziger neuer String. Wie viel+=in einer Schleife kopiert, hängt von der Engine ab, währendjoinüberall vorhersehbar ist.
Sieh es dir an
Ändern mit slice
Ein String ändert sich nicht an Ort und Stelle, deshalb ist jede Änderung ein neuer String, gebaut aus Teilen des alten. Hier ist eine Kartennummer für den Druck auf dem Kassenbon:
let card = "4532015112830366";card = card.slice(0, 12) + " " + card.slice(12);card = card.slice(0, 8) + " " + card.slice(8);card = card.slice(0, 4) + " " + card.slice(4);console.log(card);card = "**** **** ****" + card.slice(14);console.log(card);
let phone = "0888-123-456";phone = phone.slice(0, 8) + phone.slice(9);phone = phone.slice(0, 4) + phone.slice(5);console.log(phone);console.log([..."regal"].reverse().join(""));4532 0151 1283 0366**** **** **** 03660888123456lagercard.slice(0, 12) sind die Zeichen vom Anfang bis zum Index 12, ohne ihn. card.slice(12) geht vom Index 12 bis zum Ende.
| Du willst | Du schreibst |
|---|---|
x vor dem Index i einfügen |
s.slice(0, i) + x + s.slice(i) |
die Zeichen von a bis b (ohne b) durch x ersetzen |
s.slice(0, a) + x + s.slice(b) |
das Zeichen mit dem Index i entfernen |
s.slice(0, i) + s.slice(i + 1) |
die Zeichen von a bis b (ohne b) entfernen |
s.slice(0, a) + s.slice(b) |
| den Text umdrehen, von hinten nach vorne | [...s].reverse().join("") |
| die Länge, ein Zeichen | s.length, s[i] |
[...s] macht ein Array aus den Zeichen des Strings. Das Array hat reverse(), und join("") klebt es wieder zusammen.
Die Leerzeichen in der Kartennummer kommen von hinten hinein: zuerst bei 12, dann bei 8, dann bei 4. Wenn du bei 4 anfängst, verschiebt das eingefügte Leerzeichen alles dahinter, und 8 ist nicht mehr die Stelle, die du willst. Die Bindestriche in der Telefonnummer werden aus demselben Grund zuerst von hinten entfernt.
Probier es aus
Zwei Namen, ein Objekt
Was gibt das Programm aus?
const note = ["Tisch 4"];const copy = note;copy.push(", 2 Kaffee");console.log(note.join(""));
const text = "Tisch 4";let other = text;other += ", 2 Kaffee";console.log(text);copy = note kopiert den Pfeil, beide Variablen zeigen auf dasselbe Array, und push ändert es direkt. Bei einem String ist es anders: other += ... macht einen neuen String und lenkt nur other darauf um. text zeigt weiter auf den alten.Die Idee
In alphabetischer Reihenfolge: localeCompare
=== sagt dir nur „sind sie gleich“. Zum Sortieren brauchst du mehr: welcher String vor dem anderen kommt. Das sagt dir localeCompare:
console.log("Albrecht".localeCompare("Bauer", "de"));console.log("Bauer".localeCompare("Bauer", "de"));console.log("Albrecht" < "Bauer");console.log("neumann" < "Neumann");-10truefalse- eine negative Zahl: Der erste kommt vor dem zweiten;
- 0: Sie sind gleich;
- eine positive Zahl: Der erste kommt nach dem zweiten.
"de" sagt, dass nach den Regeln der deutschen Sprache verglichen wird. Du kannst Strings auch mit < und > vergleichen, aber dann geht es nach dem Code jedes Zeichens: Großbuchstaben kommen vor Kleinbuchstaben, deshalb ist "neumann" < "Neumann" false.
Wichtig ist das Vorzeichen, nicht die Zahl selbst. Damit sortierst du eine Liste von Namen, zum Beispiel mit dem Bubblesort aus der Lektion „Arrays“:
const names = ["Weber", "Albrecht", "Neumann", "Bauer"];for (let i = 0; i < names.length - 1; i++) { for (let j = 0; j < names.length - 1 - i; j++) { if (names[j].localeCompare(names[j + 1], "de") > 0) { const tmp = names[j]; names[j] = names[j + 1]; names[j + 1] = tmp; } }}console.log(names.join(", "));Albrecht, Bauer, Neumann, Webernames.join(", ") klebt die Elemente des Arrays mit einem Trennzeichen dazwischen zusammen.
Genauer, für Neugierige
localeComparemit den Regeln von"de"stellt den Kleinbuchstaben vor den Großbuchstaben:"neumann".localeCompare("Neumann", "de")ist -1. Mit{ sensitivity: "base" }achtet es nicht auf Groß- und Kleinschreibung und ergibt 0.- Mit
{ numeric: true }vergleicht es die Zahlen im Text als Zahlen: Dann kommt"INV-10"nach"INV-9". names.sort()sortiert das Array ohne den Bubblesort, aber nach den Codes der Zeichen, wie<. Für diese Namen ist das Ergebnis gleich.
console.log("neumann".localeCompare("Neumann", "de"));console.log("neumann".localeCompare("Neumann", "de", { sensitivity: "base" }));console.log("INV-10".localeCompare("INV-9", "de", { numeric: true }));const names = ["Weber", "Albrecht", "Neumann", "Bauer"];names.sort();console.log(names.join(", "));-101Albrecht, Bauer, Neumann, WeberProbier es aus
Rechnung 10 oder 9?
Die Rechnungsnummern sind Text. Welche ist früher?
Was gibt das Programm aus?
const a = "INV-10";const b = "INV-9";console.log(a < b);console.log(Number(a.slice(4)) < Number(b.slice(4)));Text wird Zeichen für Zeichen verglichen. Die ersten vier Zeichen sind gleich, dann kommen „1“ und „9“, und „1“ kommt vor „9“. Deshalb kommt "INV-10" vor "INV-9", genau wie „ba“ im Wörterbuch vor „c“ kommt. Die Zahlen 10 und 9 werden erst nach Number() als Zahlen verglichen. Wenn du Nummern als Text sortierst, fülle sie mit Nullen auf: INV-009, INV-010.
Die Idee
Suchen, zerschneiden, zusammensetzen
Hier ist eine Zeile aus einer Kontaktdatei: Name, E-Mail und Stadt, getrennt durch „ ; “, mit überflüssigen Leerzeichen an den Enden:
const line = " Maria Becker ; maria.becker@firma.de ; Köln ";const parts = line.trim().split(" ; ");const email = parts[1];const at = email.indexOf("@");console.log(email.slice(0, at));console.log(email.slice(at + 1));console.log(email.indexOf(".", at));console.log(parts.join(" | "));console.log("=".repeat(20));maria.beckerfirma.de18Maria Becker | maria.becker@firma.de | Köln====================trim()entfernt die Leerzeichen vorne und hinten und auch die „breiten“ Leerzeichen aus anderen Sprachen.indexOf("@")gibt den Index des ersten Vorkommens, undindexOf(".", at)sucht erst ab dem Indexat.lastIndexOfsucht von hinten.email.slice(0, at)geht vom Anfang bisat, ohne es.email.slice(at + 1)geht vonat + 1bis zum Ende.joinist das Gegenteil vonsplit."=".repeat(20)wiederholt den Text 20-mal.
Leer und „leer aussehend“ sind nicht dasselbe:
const empty = "";const spaces = " ";console.log(empty.length === 0, spaces.length === 0);console.log(empty.trim() === "", spaces.trim() === "");true falsetrue truelength === 0 ist nur für leeren Text wahr. trim() === "" ist auch wahr, wenn nur Leerzeichen darin stehen. Um ein Feld zu prüfen, das jemand ausgefüllt hat, willst du meistens das Zweite.
Die Idee
Zahlen und Text
Zahlen kommen in Text hinein und aus Text heraus:
const code = Number("0042");const price = Number("2.40");const label = String(code) + "-" + String(price);console.log(label);42-2.4Number() kennst du schon. String() ist das Gegenteil: Es macht Text aus einer Zahl oder einem boolean.
Für eine Tabelle mit Spalten brauchst du eine Breite für jede Spalte. Hier ist ein Kassenbon aus einem Café:
const names = ["Kaffee", "Croissant", "Mineralwasser"];const prices = [2.4, 3.1, 1.5];const qty = [2, 1, 3];let total = 0;for (let i = 0; i < names.length; i++) { const amount = prices[i] * qty[i]; total += amount; console.log(`${names[i].padEnd(16)}${String(qty[i]).padStart(2)} x ${prices[i].toFixed(2).padStart(5)} = ${amount.toFixed(2).padStart(6)}`);}console.log("-".repeat(35));console.log(`${"Gesamt".padEnd(26)}${total.toFixed(2).padStart(9)} €`);Kaffee 2 x 2.40 = 4.80Croissant 1 x 3.10 = 3.10Mineralwasser 3 x 1.50 = 4.50-----------------------------------Gesamt 12.40 €padEnd(16)füllt den Text rechts mit Leerzeichen auf 16 Zeichen auf: Er ist linksbündig.padStart(5)füllt links auf: Er ist rechtsbündig.toFixed(2)macht Text aus der Zahl, mit 2 Stellen nach dem Punkt.String(qty[i])ist nötig, weilpadStarteine Methode eines Strings ist, nicht einer Zahl.
Genauer, für Neugierige
a.concat(b)ist dasselbe wiea + b. Du siehst es in älterem Code.padEndundpadStartschneiden längeren Text nicht ab: Er schiebt die Spalte nach rechts.Number("2,40")hält das Programm nicht an. Es ergibtNaN(„not a number“, keine Zahl), und das Programm macht damit weiter.toFixedschreibt immer einen Punkt, währendtoLocaleString("de")nach den deutschen Regeln schreibt, mit Komma.
console.log(Number("2,40"));console.log((2.4).toFixed(2));console.log((2.4).toLocaleString("de", { minimumFractionDigits: 2 }));console.log("Kaffee".concat(" und ", "Croissant"));NaN2.402,40Kaffee und CroissantProbier es aus
Finde den Fehler
„neben“ liest sich rückwärts genauso, aber das Programm sagt „neben ist kein Palindrom“. In welcher Zeile ist der Fehler?
Wähle die Zeile mit dem Fehler.
[...word].reverse() ist ein Array von Zeichen, und word ist ein String. === zwischen einem String und einem Array ist false, egal was darin steht. Die Korrektur ist [...word].reverse().join(""): Damit wird das Array zu einem String.Übung
Text, den du zusammensetzt
Löse die Aufgaben in deinem Editor. Gib keine Eingabeaufforderungen aus.
1. Palindrom. Lies eine Zeile und sag, ob sie ein Palindrom ist, ohne auf Großbuchstaben, Leerzeichen, Kommas und Bindestriche zu achten. Dreh den Text diesmal mit dem Ausdruck [...text].reverse().join("") um statt mit einer Schleife.
Erika feuert nur untreue FakirePalindromErika feuert nur treue FakireKein Palindrom2. Ein Kassenbon mit Spalten. In der ersten Zeile steht die Anzahl der Artikel. In jeder Zeile danach stehen ein Name, ein Preis und eine Menge, getrennt durch ;. Gib einen Kassenbon mit ausgerichteten Spalten und der Summe am Ende aus.
3Brot;1.20;2Käse;9.80;1Eier;0.35;10Brot 2 x 1.20 = 2.40Käse 1 x 9.80 = 9.80Eier 10 x 0.35 = 3.50------------------------------------Gesamt 15.703. Wörter rückwärts. Lies einen Satz und gib seine Wörter in umgekehrter Reihenfolge aus. Zwischen den Wörtern kann mehr als ein Leerzeichen stehen, und im Ergebnis muss genau eines stehen. Setz es mit einem Array und join zusammen.
Morgen um 9 ist eine BesprechungBesprechung eine ist 9 um MorgenWeitere Aufgaben
- Initialen. Lies einen vollen Namen („Maria Luise Becker“) und gib die Initialen mit Punkten aus: „M. L. B.“ Setz sie mit einem Array und
joinzusammen. - Versteckte E-Mail. Mach aus
maria.becker@firma.dedie Adressem***********@firma.de: der erste Buchstabe, dann so viele Sternchen, wie es andere Zeichen vor dem@gibt. NimmindexOfundrepeat. - Eine Runde durch Strings. Erzeuge einen String mit Anführungszeichen (
"Beispiel eins") und einen anderen mitnew String("Beispiel eins"). Vergleiche sie mit===und mit==, nimmslice(0, 8), trenne den ersten beim Leerzeichen und gib die Teile aus, mach aus"100"eine Zahl und gib sie mit einem Template-Literal`Wert: ${...}`aus, und füge schließlich" und mehr"zu einem Array mit dem ersten String hinzu und klebe sie mitjoinzusammen. Sag jede Zeile voraus, bevor du das Programm startest. (new Stringmacht ein Objekt statt eines Strings. Echter Code nutzt es nicht.) - Sortieren ohne Groß- und Kleinschreibung. Ändere den Bubblesort von der Karte zu
localeCompareso, dass er{ sensitivity: "base" }übergibt, und probier ihn mit „lena“, „Jonas“ und „anna“ aus.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Strings: mit Text arbeiten · Klassen und Objekte · Instanzmethoden · Vererbung
- Geht weiter in: Reguläre Ausdrücke · Reguläre Ausdrücke: Gruppen und Ersetzen · Dateien · Daten in Dateien
- Aufgaben zum Thema: Text und Dateien