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.

JavaScript grundlagen.js
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 intakt
Output
Hallo
Hallo HALLO

Single, 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.

JavaScript quote-stile.js
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 Backtick
Output
Hallo, 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.

JavaScript surrogate-pairs.js
// 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 Grapheme
Output
2
1

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

JavaScript length-falle.js
'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 Grapheme
Output
8
1

Fü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.

JavaScript indizierung.js
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);  // '😀'
Output
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)).

JavaScript iteration.js
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 Codepoints
Output
A
😀
B

Konkatenation: + vs. Template Literals

Es gibt drei gebräuchliche Wege, Strings zusammenzusetzen — alle mit unterschiedlicher Lesbarkeit und identischer Performance (Engines optimieren beide gleich).

JavaScript konkatenation.js
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 immer
Output
Hi 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.

EscapeBedeutungBeispiel
\nZeilenumbruch (LF)'a\nb'
\rCarriage Return'a\rb'
\tTab'a\tb'
\\Backslash'C:\\path'
\' / \"Quote'It\'s'
\xNNLatin-1 (1 Byte hex)'\xE4' -> 'ä'
\uNNNNUTF-16 Code-Unit (4 hex)'ä' -> 'ä'
\u{NNNNN}Codepoint, beliebig (ES6+)'\u{1F600}' -> '😀'
\0Null-Byte'a\0b'
JavaScript escapes.js
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
Output
ä
😀
🇩🇪

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.

JavaScript wrapper-anti-pattern.js
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 — schlecht
Output
string
object

Hä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: &#36;{. 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.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Datentypen

Zur Übersicht