Web Dev Notes

Understanding Unicode in JavaScript

Two strings that look identical on screen can be different byte-for-byte. Most Unicode confusion in JavaScript comes from three sources: escape syntax, encoding, and normalization.

Four ways to write the same character

"A"          // literal
"\u0041"     // 4-digit UTF-16 unit escape
"\u{41}"     // braced code point escape (ES6)
String.fromCodePoint(0x41)  // "A"

The braced form is the only one that takes code points above U+FFFF directly: "\u{1F600}" is the emoji, while "\uD83D\uDE00" is its surrogate-pair spelling.

Length is not what you think

.length counts UTF-16 units. Emoji, accented characters from some scripts, and CJK ideographs outside the BMP count as 2 (or more, for ZWJ sequences):

"a".length;              // 1
"\u{1F600}".length;      // 2
"πŸ‘¨β€πŸ‘©β€πŸ‘§".length;           // 8 (family emoji: 4 people + 3 ZWJ)

To count user-perceived characters, use the iterator or Intl.Segmenter:

[..."πŸ‘¨β€πŸ‘©β€πŸ‘§"].length;                        // 4 code points
new Intl.Segmenter().segment("πŸ‘¨β€πŸ‘©β€πŸ‘§").length !== undefined
// iterate grapheme segments for the true count

Normalization: why equality fails

The same visual text can be encoded as a single code point or as a base letter plus combining marks. "Γ©" exists both ways:

const a = "\u00E9";            // precomposed Γ©
const b = "e\u0301";           // e + combining acute
a === b;                       // false!
a.length;                      // 1
b.length;                      // 2

Normalize before comparing or indexing:

a.normalize("NFC") === b.normalize("NFC");  // true

NFC composes where possible (recommended for storage), NFD decomposes (useful for stripping accents), and NFKC/NFKD additionally unify compatibility forms like β‘  β†’ 1.

Accent-insensitive search, the short way

function fold(s) {
  return s.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
}
fold("Crème brûlée"); // "creme brulee"

Checklist

See also