Skip to content

09 — Strings and Unicode

One line: A JavaScript string is a list of UTF-16 code units, so length, iteration, comparison, case and bytes each have a code-unit answer that differs from what a reader would call a character.

The chapter builds the model first. A string is UTF-16 code units; above them sit code points, which for...of walks; above those, graphemes, which only Intl.Segmenter finds; and normalization decides whether two spellings of one text are equal. The everyday tools follow in the light of that model: template literals, comparing and sorting, converting to and from bytes, the common string methods, and case conversion last, because it needs every earlier page.

The pages

Page Level In one line Status
Strings are UTF-16 101 A string is a sequence of 16-bit code units and length counts them: "é" is 1 or 2 long depending on whether the accent is its own code point, "😀".length is 2, and "😀"[0] is half an emoji, the lone surrogate 0xD83D. stub
Code points 201 for...of, spread and Array.from walk a string by code point, so [..."a😀"] has 2 items where length and split("") count 3; indexes still count code units, and codePointAt(2) on the same string returns the lone low surrogate 0xde00 without complaint. stub
Graphemes 201 What a reader sees as one character can be several code points: a flag is 2, a family emoji 5 (and 8 code units); only Intl.Segmenter with granularity: "grapheme" counts each as 1, using the Unicode rules of the ICU that Node ships. stub
Normalization 201 "\u{E9}" and "e\u{301}" both show as é, yet === is false, their lengths are 1 and 2, and a Set keeps both; normalize("NFC") on each side makes them equal, while localeCompare already returns 0 for the pair. stub
Template literals 101 ${x} converts with the string hint and "" + x with the default hint, so an object whose valueOf returns 1 and toString returns "two" gives "two" in a template and "1" after +; a symbol makes a template throw where String(symbol) works. stub
Comparing and sorting strings 201 < and a bare sort() compare UTF-16 code units, so "Z" < "a", "Éclair" sorts after "zebra", and "😀" < "\u{FF61}" though U+1F600 is the larger code point; localeCompare and Intl.Collator follow a locale's alphabet, and numeric: true puts item9 before item10. stub
Strings and bytes 201 TextEncoder only writes UTF-8 and silently turns a lone surrogate into ef bf bd, the bytes of U+FFFD, so an ill-formed string does not survive the trip to bytes; TextDecoder also swaps bad bytes for U+FFFD unless fatal: true makes it throw a TypeError. stub
String methods 101 "hello".slice(-3) is "llo" but substring(-3) is the whole string, because substring treats negatives as 0 and swaps reversed ends; and a replacement string still has special patterns: "a.b".replaceAll(".", "$&$&") is "a..b", since $& means the matched text. stub
Case conversion 201 Case mapping follows Unicode's full rules, so it can change length: "straße".toUpperCase() is "STRASSE" and "İ".toLowerCase() is two code units; lowercasing both sides therefore misses that "straße" and "STRASSE" match, which localeCompare with sensitivity: "base" finds. stub

Boundaries

Regular expressions, including the u and v flags, have their own chapter; Node's Buffer and reading files with an encoding are in The Node.js runtime; tag functions are in Metaprogramming.