Zum Inhalt
wannacode

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);
Ausgabe
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);
Ausgabe
Auftrag 1: 12.50 €
Auftrag 2: 12.50 €
Auftrag 3: 12.50 €
  • const parts = [] ist ein leeres Array. push hä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 du parts.join("").
  • Die neue Zeile zwischen den Stücken kommt vom Trennzeichen, deshalb haben die Stücke hier kein \n am 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
  1. report zeigt auf einen leeren String.
  2. Das erste += macht einen neuen String mit einer Zeile: 19 Zeichen.
  3. 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.
  4. 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.
  5. parts ist ein leeres Array: Darin landen die Stücke.
  6. push legt jedes Stück direkt in das Array. Die alten Stücke werden nicht kopiert, das Array bleibt dasselbe.
  7. 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ährend join ü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(""));
Ausgabe
4532 0151 1283 0366
**** **** **** 0366
0888123456
lager

card.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);
Antwort

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");
Ausgabe
-1
0
true
false
  • 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(", "));
Ausgabe
Albrecht, Bauer, Neumann, Weber

names.join(", ") klebt die Elemente des Arrays mit einem Trennzeichen dazwischen zusammen.

Genauer, für Neugierige
  • localeCompare mit 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(", "));
Ausgabe
-1
0
1
Albrecht, Bauer, Neumann, Weber

Probier 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)));
Antwort

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));
Ausgabe
maria.becker
firma.de
18
Maria 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, und indexOf(".", at) sucht erst ab dem Index at. lastIndexOf sucht von hinten.
  • email.slice(0, at) geht vom Anfang bis at, ohne es. email.slice(at + 1) geht von at + 1 bis zum Ende. join ist das Gegenteil von split.
  • "=".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() === "");
Ausgabe
true false
true true

length === 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);
Ausgabe
42-2.4

Number() 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)} €`);
Ausgabe
Kaffee 2 x 2.40 = 4.80
Croissant 1 x 3.10 = 3.10
Mineralwasser 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, weil padStart eine Methode eines Strings ist, nicht einer Zahl.
Genauer, für Neugierige
  • a.concat(b) ist dasselbe wie a + b. Du siehst es in älterem Code.
  • padEnd und padStart schneiden längeren Text nicht ab: Er schiebt die Spalte nach rechts.
  • Number("2,40") hält das Programm nicht an. Es ergibt NaN („not a number“, keine Zahl), und das Programm macht damit weiter. toFixed schreibt immer einen Punkt, während toLocaleString("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"));
Ausgabe
NaN
2.40
2,40
Kaffee und Croissant

Probier 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.

Ü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.

Eingabe
Erika feuert nur untreue Fakire
Ausgabe
Palindrom
Eingabe
Erika feuert nur treue Fakire
Ausgabe
Kein Palindrom

2. 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.

Eingabe
3
Brot;1.20;2
Käse;9.80;1
Eier;0.35;10
Ausgabe
Brot 2 x 1.20 = 2.40
Käse 1 x 9.80 = 9.80
Eier 10 x 0.35 = 3.50
------------------------------------
Gesamt 15.70

3. 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.

Eingabe
Morgen um 9 ist eine Besprechung
Ausgabe
Besprechung eine ist 9 um Morgen

Weitere 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 join zusammen.
  • Versteckte E-Mail. Mach aus maria.becker@firma.de die Adresse m***********@firma.de: der erste Buchstabe, dann so viele Sternchen, wie es andere Zeichen vor dem @ gibt. Nimm indexOf und repeat.
  • Eine Runde durch Strings. Erzeuge einen String mit Anführungszeichen ("Beispiel eins") und einen anderen mit new String("Beispiel eins"). Vergleiche sie mit === und mit ==, nimm slice(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 mit join zusammen. Sag jede Zeile voraus, bevor du das Programm startest. (new String macht ein Objekt statt eines Strings. Echter Code nutzt es nicht.)
  • Sortieren ohne Groß- und Kleinschreibung. Ändere den Bubblesort von der Karte zu localeCompare so, dass er { sensitivity: "base" } übergibt, und probier ihn mit „lena“, „Jonas“ und „anna“ aus.

Test

Teste dich

1. Warum ist report += line in Node in einer Schleife mit 50.000 Durchläufen nicht langsam?
2. Was gibt "Bauer".localeCompare("Albrecht", "de") zurück?
3. const a = ["12"]; const b = a; b.push("3"); Was ergibt a.join("")?
4. parts ist ein Array von Strings. Wie machst du daraus einen einzigen String ohne Trennzeichen?
5. Was ergibt (2.4).toFixed(2).padStart(6)?

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