Strings sind in JavaScript UTF-16-Sequenzen von Code-Units, immutable und mit drei verschiedenen Quote-Stilen schreibbar. Das klingt simpel — bis Emojis ins Spiel kommen. Ein einzelnes 😀 hat length === 2, weil es als Surrogate Pair aus zwei UTF-16-Code-Units gespeichert wird. Wer Strings korrekt iteriert, zählt oder validiert, muss den Unterschied zwischen Code-Units, Codepoints und Graphemen kennen. Dieser Artikel klärt das Speicher-Modell, zeigt die häufigsten Fallen und erklärt, warum new String('x') ein Anti-Pattern ist.
Sequenz von UTF-16-Code-Units
Ein JavaScript-String ist intern eine Sequenz von 16-Bit-Werten — sogenannten Code-Units. Das Speicher-Modell ist UTF-16, festgelegt seit der ersten ECMAScript-Spezifikation 1997. Jeder Code-Point der Basic Multilingual Plane (U+0000 bis U+FFFF) belegt eine Code-Unit; alles darüber (Emojis, seltene CJK-Zeichen, mathematische Symbole) belegt zwei Code-Units als Surrogate Pair.
Strings sind immutable — jede „Änderung" gibt einen neuen String zurück. Index-Zuweisung wie str[0] = 'x' wird im Sloppy Mode stillschweigend ignoriert, im Strict Mode wirft sie TypeError.
const s = 'Hallo';
typeof s; // 'string'
s.length; // 5
// Immutability
s[0] = 'X'; // sloppy: still ignoriert; strict: TypeError
console.log(s); // 'Hallo' — unverändert
// jede Methode gibt neuen String zurück
const upper = s.toUpperCase();
console.log(s, upper); // 'Hallo' 'HALLO' — Original intaktHallo
Hallo HALLOSingle, Double, Backtick
JavaScript erlaubt drei Quote-Zeichen für String-Literale. Single ('…') und Double ("…") sind funktional identisch — die Wahl ist Style-Convention. Backticks (`…`) erzeugen Template Literals mit zwei Zusatz-Features: eingebettete Expressions via ${…} und mehrzeilige Strings ohne \n-Escapes.
const a = 'single';
const b = "double";
const c = `template`;
// Template-Features
const name = 'Welt';
const greet = `Hallo, ${name}!`; // Interpolation
const multi = `Zeile 1
Zeile 2
Zeile 3`; // Mehrzeilig, kein \n nötig
// Escape-Regeln pro Stil
const sing = 'It\'s'; // Single in Single
const doub = "say \"hi\""; // Double in Double
const tick = `und \`code\``; // Backtick in BacktickHallo, Welt!Die Convention variiert: Prettier-Default ist Double, Airbnb-Style ist Single. Wichtiger als die Wahl ist Konsistenz — Linter erzwingen das automatisch.
Code-Units, Codepoints, Surrogate Pairs
Unicode definiert über 1,1 Millionen mögliche Codepoints — UTF-16 kann mit 16 Bit nur 65.536 davon direkt darstellen. Für höhere Codepoints (U+10000 und drüber, die Supplementary Planes) wird ein Surrogate Pair verwendet: zwei spezielle Code-Units, die zusammen einen Codepoint kodieren. Jedes Emoji jenseits der ältesten BMP-Symbole gehört in diesen Bereich.
// BMP-Zeichen: 1 Code-Unit
'a'.length; // 1
'ä'.length; // 1
'€'.length; // 1 (U+20AC)
// Supplementary Plane: 2 Code-Units (Surrogate Pair)
'😀'.length; // 2 (U+1F600)
'𝕏'.length; // 2 (U+1D54F, mathematischer Großbuchstabe X)
'🇩🇪'.length; // 4 (Flag = 2 Regional-Indikatoren je 2 Code-Units)
// echte Codepoint-Anzahl
[...'😀'].length; // 1
[...'🇩🇪'].length; // 2 — 2 Codepoints, aber 1 Grapheme2
1Wenn length nicht das tut, was man denkt
Die length-Property zählt UTF-16-Code-Units, nicht visuelle Zeichen. Für ASCII-Text gibt das die erwartete Antwort. Sobald Emojis, Flaggen oder Hautton-Modifier auftauchen, weicht das Ergebnis vom Erwarteten ab — manchmal dramatisch.
'hi'.length; // 2 — wie erwartet
'😀'.length; // 2 — Surrogate Pair
'👨👩👧'.length; // 8 — ZWJ-Sequenz: 3 Personen + 2 ZWJ-Joiner
'🇩🇪'.length; // 4 — Flag = 2 Regional Indicator Symbols
// korrekte Codepoint-Anzahl
[...'😀'].length; // 1
[...'👨👩👧'].length; // 5 — Personen + ZWJ als einzelne Codepoints
// visuelle Grapheme (Intl.Segmenter, ES2022)
const seg = new Intl.Segmenter('de', { granularity: 'grapheme' });
[...seg.segment('👨👩👧')].length; // 1 — ein Grapheme
[...seg.segment('🇩🇪')].length; // 1 — ein Grapheme8
1Für ein Eingabefeld mit Zeichen-Limit ist length fast nie das richtige. Twitter zählt Codepoints ([...str].length), iMessage zählt Grapheme. Wer einfach str.length nimmt, lässt User mit Emoji-reichen Eingaben unverdient früh ans Limit stoßen.
charAt, charCodeAt, codePointAt
Es gibt drei Methoden für den Zugriff auf einzelne Positionen — und sie tun nicht dasselbe.
const s = 'A😀B';
// Bracket / charAt: gibt Code-Unit als String zurück
s[0]; // 'A'
s.charAt(0); // 'A'
s[1]; // '\uD83D' — halbes Surrogate!
s.charAt(1); // '\uD83D'
// charCodeAt: numerischer Wert der Code-Unit
s.charCodeAt(0); // 65 (U+0041)
s.charCodeAt(1); // 55357 (high surrogate)
// codePointAt: gibt echten Codepoint, auch über Surrogate-Grenze
s.codePointAt(0); // 65 — 'A'
s.codePointAt(1); // 128512 — 0x1F600 = 😀
s.codePointAt(2); // 56832 — low surrogate als Fallback
// String.fromCodePoint kehrt um
String.fromCodePoint(0x1F600); // '😀'128512
😀Faustregel: für moderne Unicode-Verarbeitung immer codePointAt/fromCodePoint nutzen, nicht die alten charCodeAt/fromCharCode. Der Unterschied wird erst bei Supplementary-Plane-Zeichen sichtbar — und genau da bricht naiver Code.
for…of und Spread iterieren über Codepoints
String.prototype[Symbol.iterator] iteriert über Codepoints, nicht über Code-Units. Das gilt für for…of, Array-Spread [...str] und alle Iterable-Konsumenten (Array.from, Array.from(str)).
const s = 'A😀B';
// klassische for-i Schleife: Code-Units (kann Surrogates zerschneiden)
for (let i = 0; i < s.length; i++) {
console.log(s[i]); // 'A', '\uD83D', '\uDE00', 'B'
}
// for-of: korrekte Codepoint-Iteration
for (const ch of s) {
console.log(ch); // 'A', '😀', 'B'
}
// Spread / Array.from: ebenfalls codepoint-basiert
[...s]; // ['A', '😀', 'B']
Array.from(s); // ['A', '😀', 'B']
// Achtung: nur Codepoints, nicht Grapheme!
[...'👨👩👧']; // 5 Einträge — ZWJ-Joiner als eigene CodepointsA
😀
BKonkatenation: + vs. Template Literals
Es gibt drei gebräuchliche Wege, Strings zusammenzusetzen — alle mit unterschiedlicher Lesbarkeit und identischer Performance (Engines optimieren beide gleich).
const name = 'Anna';
const age = 28;
// 1) Plus-Operator — laut, viele Quotes
const a = 'Hi ' + name + ', du bist ' + age + ' Jahre alt.';
// 2) Template Literal — modern, lesbar
const b = `Hi ${name}, du bist ${age} Jahre alt.`;
// 3) Array.join — sinnvoll bei vielen Stücken in Schleife
const teile = [];
for (let i = 0; i < 100; i++) teile.push(`Item ${i}`);
const c = teile.join('\n');
// Performance: alle drei sind in modernen Engines gleich schnell
// Lesbarkeit: Template gewinnt fast immerHi Anna, du bist 28 Jahre alt.Der Mythos „+ ist langsam, immer Array.join nutzen" stammt aus IE6-Zeiten. V8, SpiderMonkey und JavaScriptCore haben Rope-Strings und Lazy-Concatenation — die Wahl der Syntax ist heute reine Stilfrage.
Steuerzeichen und Unicode-Escapes
Strings unterstützen mehrere Escape-Formen für nicht-darstellbare Zeichen oder solche, die mit dem Quote-Zeichen kollidieren würden.
| Escape | Bedeutung | Beispiel |
|---|---|---|
\n | Zeilenumbruch (LF) | 'a\nb' |
\r | Carriage Return | 'a\rb' |
\t | Tab | 'a\tb' |
\\ | Backslash | 'C:\\path' |
\' / \" | Quote | 'It\'s' |
\xNN | Latin-1 (1 Byte hex) | '\xE4' -> 'ä' |
\uNNNN | UTF-16 Code-Unit (4 hex) | 'ä' -> 'ä' |
\u{NNNNN} | Codepoint, beliebig (ES6+) | '\u{1F600}' -> '😀' |
\0 | Null-Byte | 'a\0b' |
const u = 'ä'; // 'ä' — alt, max U+FFFF
const c = '\u{1F600}'; // '😀' — Curly-Form für > U+FFFF
const t = '\u{1F1E9}\u{1F1EA}'; // '🇩🇪' — zwei Codepoints
// Curly-Form ist nicht nur kürzer, sie ist nötig für > U+FFFF.
// Ohne Curly muss man das Surrogate Pair selbst schreiben:
const alt = '😀'; // '😀' — high + low surrogateä
😀
🇩🇪new String() — niemals nutzen
Wie alle Primitives hat String einen Object-Wrapper: new String('x') erzeugt ein Objekt, kein Primitive. Der Unterschied ist tückisch — viele Operationen funktionieren scheinbar identisch, aber typeof, === und JSON-Serialisierung verhalten sich anders.
const prim = 'hi';
const obj = new String('hi');
typeof prim; // 'string'
typeof obj; // 'object' (!)
prim === 'hi'; // true
obj === 'hi'; // false — anderer Typ
obj == 'hi'; // true — loose equality coerced
// alle Objekte sind truthy — auch new Boolean(false) und new String('')
Boolean(new String('')); // true (!)
// JSON-Verhalten
JSON.stringify(prim); // '"hi"'
JSON.stringify(obj); // '"hi"' — sieht gleich aus, ist aber Object
// gute Nachricht: niemand nutzt new String() ernsthaft
// String('x') OHNE new ist anders: explizite Coercion zu Primitive
String(42); // '42' — Primitive, gut
new String(42); // String-Object — schlechtstring
objectHäufige Fragen zu Strings
Warum ist '😀'.length === 2?
Weil length UTF-16-Code-Units zählt, nicht Codepoints. Das Emoji liegt außerhalb der Basic Multilingual Plane (U+1F600) und wird als Surrogate Pair aus zwei Code-Units kodiert. Für die echte Anzahl Codepoints: [...str].length. Für visuelle Grapheme (User-perceived characters) Intl.Segmenter mit granularity: 'grapheme'.
Wie zähle ich Zeichen wirklich korrekt?
Es gibt drei Antworten: str.length für Code-Units (technisch), [...str].length für Codepoints (Twitter-Limit-Style), und Intl.Segmenter für Grapheme (was der User sieht). Welche „korrekt" ist, hängt vom Anwendungsfall ab — für UI-Zeichen-Limits sind Grapheme die ehrlichste Wahl.
Soll ich Single oder Double Quotes nutzen?
Funktional identisch. Prettier-Default ist Double, Airbnb-Style ist Single, StandardJS ebenfalls Single. Konsistenz innerhalb einer Codebasis ist wichtiger als die Wahl. Backticks nur dann, wenn man Interpolation oder Multi-Line braucht — sonst sind sie unnötiger Noise.
Sind Strings veränderbar?
Nein, Strings sind immutable. str[0] = 'X' ist im Sloppy Mode ein stiller No-Op, im Strict Mode ein TypeError. Jede Methode wie toUpperCase oder replace gibt einen neuen String zurück, der Original-String bleibt unangetastet. Das ist ein wichtiger Unterschied zu Arrays, die mutable sind.
Warum new String() niemals nutzen?
Es erzeugt einen Object-Wrapper statt ein Primitive: typeof ist 'object', der strikte Vergleich === 'hi' schlägt fehl, und alle Objects sind truthy — auch new String(''). Die Funktion String(value) ohne new ist dagegen die korrekte explizite Coercion zu einem Primitive.
Wie konkateniere ich viele Strings effizient?
In modernen Engines (V8, SpiderMonkey, JSC) sind +, Template Literals und Array.join alle gleich schnell — Engines nutzen Rope-Strings und Lazy-Concatenation. Die Wahl ist Stilfrage. Bei sehr vielen Stücken in einer Schleife ist Array.join oft am lesbarsten, bei strukturierten Texten Template Literals.
Wie escape ich Backticks in Template Literals?
Mit Backslash: ` innerhalb der Backticks. Innerhalb von Template Literals müssen außerdem ${-Sequenzen escaped werden, wenn sie als Literal-Text gemeint sind: ${. Single- und Double-Quotes brauchen in Templates dagegen kein Escape.
Multi-Line-Strings ohne \\n?
Ja — Template Literals. Jeder Zeilenumbruch im Source wird zu einem \n im Wert. '…'- und "…"-Literale erlauben Mehrzeiler nur mit explizitem \n oder dem Line-Continuation-Backslash am Zeilenende (Letzteres ist Style-mäßig verpönt). Vor Template Literals war ['Zeile 1', 'Zeile 2'].join('\n') der Workaround.