UTF-16 Code Units vs Code Points in JavaScript
JavaScript internally encodes strings as sequences of 16-bit UTF-16 code units. This architectural design means that while characters within the Basic Multilingual Plane (BMP) map directly to a single code unit, supplementary characters—such as emojis, mathematical symbols, and historical scripts—require two code units, known as a surrogate pair. This article explores how JavaScript distinguishes between UTF-16 code units and Unicode code points, the common pitfalls with legacy string methods, and the modern features designed to handle full Unicode accurately.
Code Units vs. Code Points: The Core Difference
A Unicode code point is a numerical value assigned
to a specific abstract character in the Unicode standard, ranging from
U+0000 to U+10FFFF.
A UTF-16 code unit is a 16-bit binary value used to
encode code points: * Basic Multilingual Plane (BMP):
Covers code points from U+0000 to U+FFFF.
These fit into a single 16-bit code unit. * Supplementary
Planes: Cover code points from U+010000 to
U+10FFFF. These cannot fit into 16 bits, so UTF-16 encodes
them as a pair of 16-bit units called a surrogate pair
(one high surrogate and one low surrogate).
Legacy JavaScript: Operating on Code Units
Historically, ECMAScript defined strings strictly as sequences of 16-bit elements. As a result, standard property lookups and legacy methods operate on UTF-16 code units rather than Unicode code points.
1. String Length
The .length property returns the number of UTF-16 code
units, not the number of rendered characters or code points.
const char = "A"; // BMP: U+0041
console.log(char.length); // 1
const emoji = "😀"; // Supplementary: U+1F600 (encoded as \uD83D\uDE00)
console.log(emoji.length); // 22. Indexing and charAt()
Bracket notation (str[index]) and
.charAt(index) return the code unit at the specified
position. Accessing an index of a surrogate pair splits the character
and returns an isolated, invalid surrogate.
const emoji = "😀";
console.log(emoji[0]); // "\uD83D" (High surrogate, cannot be displayed alone)
console.log(emoji.charAt(0)); // "\uD83D"3. Character Codes
.charCodeAt() returns the 16-bit numeric value of the
code unit at the given index.
const emoji = "😀";
console.log(emoji.charCodeAt(0)); // 55357 (0xD83D)
console.log(emoji.charCodeAt(1)); // 56832 (0xDE00)Modern JavaScript: Code-Point Aware Features
ECMAScript 2015 (ES6) introduced native support for full Unicode code points across iterations, methods, and regular expressions.
1.
codePointAt() and String.fromCodePoint()
To retrieve the complete 21-bit Unicode value, use
codePointAt(). It reads both halves of a surrogate pair
when called on the index of the leading surrogate. Conversely,
String.fromCodePoint() converts code points into properly
encoded strings.
const emoji = "😀";
console.log(emoji.codePointAt(0)); // 128512 (0x1F600)
console.log(String.fromCodePoint(0x1F600)); // "😀"2. Iterators and Spread Syntax
JavaScript’s string iterator is code-point aware. Using
for...of or the spread operator (...) iterates
over complete Unicode code points rather than raw code units.
const text = "A😀B";
// Length in code units
console.log(text.length); // 4
// Length in code points using the spread operator
const codePoints = [...text];
console.log(codePoints); // ["A", "😀", "B"]
console.log(codePoints.length); // 33. Unicode Regular
Expressions (u and v flags)
Using the u (Unicode) or v (Unicode Sets)
flag in regular expressions ensures patterns match full code points
instead of single code units.
// Without the 'u' flag, '.' matches a single code unit:
console.log(/^.$/.test("😀")); // false
// With the 'u' flag, '.' matches a full code point:
console.log(/^.$/u.test("😀")); // trueSummary Comparison
| Operation | Code Unit Level (16-bit) | Code Point Level (Full Unicode) |
|---|---|---|
| Length Count | str.length |
[...str].length or
Array.from(str).length |
| Character Access | str[i] /
str.charAt(i) |
[...str][i] |
| Numeric Value | str.charCodeAt(i) |
str.codePointAt(i) |
| String Generation | String.fromCharCode(...) |
String.fromCodePoint(...) |
| Pattern Matching | /^.$/ |
/^.$/u |