An interactive guide to why old JavaScript methods can break modern characters.
A
Unicode character
(or "code point") is a unique number for a symbol (e.g.,
U+0041
for 'A'). The number of bytes it uses depends on the encoding:
In JavaScript (UTF-16), characters outside the basic set require a surrogate pair (two 2-byte units) to represent one symbol. Even more complex are grapheme clusters, where multiple characters (some using surrogate pairs) join to form a single visual glyph, like ๐จโ๐ฉโ๐งโ๐ฆ.
This is the root of the problem: older JS methods operate on the 2-byte units, not the final visual symbols.
| Char | Description | Type | .length |
|---|---|---|---|
| ๐ | Face with Tears of Joy | Surrogate Pair | 2 |
| ๐ฉ๐พโ๐ป | Woman Technologist | Grapheme Cluster | 7 |
| ๐จโ๐ฉโ๐งโ๐ฆ | Family | Grapheme Cluster | 11 |
The family emoji is formed by "gluing" four different emojis together with the invisible Zero Width Joiner (ZWJ) character.
The total
.length
of 11 comes from adding the seven parts: 2 + 1 + 2 + 1 + 2 + 1 + 2 = 11.
First, let's test the musical G-clef symbol (๐). It's a single visual character represented by one surrogate pair (\uD834\uDD1E).
'๐'.length
'๐'.charAt(0)
Array.from('๐')
for (const char of '๐')
/^.$/.test('๐')
(no 'u' flag)
/^.$/u.test('๐')
(with 'u' flag)
Now let's test the family emoji. It's a single visual glyph made of 7 distinct Unicode code points. This shows the limits of even some modern methods.
'๐จโ๐ฉโ๐งโ๐ฆ'.length
'๐จโ๐ฉโ๐งโ๐ฆ'.charAt(0)
Array.from('๐จโ๐ฉโ๐งโ๐ฆ')
for (const char of '๐จโ๐ฉโ๐งโ๐ฆ')
/^.$/.test('๐จโ๐ฉโ๐งโ๐ฆ')
(no 'u' flag)
/^.$/u.test('๐จโ๐ฉโ๐งโ๐ฆ')
(with 'u' flag)