Before this lesson
Know the basic use of g/u flags, test and exec. This lesson examines matching state and Unicode.
What you will be able to do
Handle non-ASCII text and avoid alternating validation results from g.
Understand the mechanism
JavaScript strings use UTF-16. A plain dot normally consumes one code unit; u handles surrogate pairs as code points. A code point still is not always a user-perceived character, such as a letter plus combining accent.
Under u, \p{L} matches Unicode letters and \p{M} combining marks. Decide separately whether digits, spaces or punctuation are permitted. Do not use \w as an international name policy.
A RegExp with g or y maintains lastIndex. Repeated test calls continue from that position and reset it after failure. Ordinary input validation usually needs neither flag.
Runnable example
console.log(/^.$/.test("😀"), /^.$/u.test("😀"));
const letters = /^\p{L}[\p{L}\p{M}]*$/u;
console.log(JSON.stringify(["学习", "café", "e\u0301", "42"].map(s => letters.test(s))));
const stateful = /a/g;
console.log(stateful.test("a"), stateful.lastIndex);
console.log(stateful.test("a"), stateful.lastIndex);
console.log(/a/.test("a"), /a/.test("a"));
Trace and verify
The emoji fails the single-dot test without u and passes with u. The letter rule accepts Chinese, precomposed é and a combining accent, rejecting 42. The g tests give true/index1, then false/index0.
Common mistakes and boundaries
u does not normalize text. Use normalize("NFC") when canonical equivalence is part of the requirement. An exec loop with empty matches can stall; prefer matchAll or explicitly handle advancement.
Practice challenge
Test precomposed é and e plus a combining accent with /^.$/u, then retry after NFC normalization.
Reference solution and reasoning
The original strings have one and two code points, giving true and false. NFC turns both into precomposed é, so both pass; this does not mean every visual character can become one code point.
Lab: Unicode length and regex state are independent
A repeated test can alternate results because g advances lastIndex, not because encoding changed. Separately, a visible character need not occupy one UTF-16 code unit. Observe both mechanisms so you do not try to fix statefulness by adding a Unicode flag.
Run this separate experiment in a modern browser Console; it is not a replacement for the main example above.
const globalPattern = /a/g;
console.log(globalPattern.test('a'), globalPattern.lastIndex);
console.log(globalPattern.test('a'), globalPattern.lastIndex);
console.log(globalPattern.test('a'), globalPattern.lastIndex);
const emoji = '😀';
console.log(emoji.length, [...emoji].length);
console.log(/^.$/.test(emoji), /^.$/u.test(emoji));
const decomposed = 'e\u0301';
console.log(decomposed === 'é', decomposed.normalize('NFC') === 'é');
Expected observations
Output is true 1, false 0, true 1; then 2 1, false true and false true.
Why this happens and where it stops applying
Ordinary validation usually does not need g. When stateful scanning is intentional, manage or reset the index explicitly. The u flag gives code-point-aware behavior to constructs such as dot, but a user-perceived character can still contain several code points, including combining marks or joined emoji. Define whether a length limit counts bytes, code points or grapheme clusters before choosing an API. Normalization must not be casually imposed on sensitive values such as passwords.
Extension challenge
Why is [...text].length still not always the right displayed character count?
Reasoned solution
It counts code points, so e plus a combining accent counts as two. For user-perceived characters, use grapheme segmentation such as Intl.Segmenter where supported, with an explicit compatibility policy. Test combining sequences, flags and skin-tone emoji as well as English.