先记住这个答案
JavaScript 字符串用 UTF-16 存储,😀 这种编号超过 U+FFFF 的字符要用两个代码单元(一对代理项)来表示。不加 u 时,正则按代码单元匹配,点号只能匹配半个 emoji,量词只作用在后半个,字符类里写 emoji 会被拆成两个字符。加上 u 之后,正则按码点匹配,这些问题都消失了,同时还能用 \u{1F600} 码点转义和 \p{...} 属性转义。代价是语法变严格,一些以前能跑的写法会直接报 SyntaxError。
- 不加
u时/^.$/匹配不了单个 emoji u让量词和字符类按码点作用,不再拆代理对\u{...}和\p{...}只在u模式下生效u按码点工作,家庭 emoji 这种组合字符照样会拆
一个点号吃不下一个 emoji
先从最直观的现象看。很多人写校验「输入只能是一个字符」时会用 /^.$/,结果用户输入一个 emoji 就校验失败了。
/^.$/,加不加 u 结果不同JavaScriptconst emoji = '😀' // U+1F600
console.log(emoji.length) // 2:底层是两个代码单元
console.log(/^.$/.test(emoji)) // false:点号只匹配了一半
console.log(/^.$/u.test(emoji)) // true:按码点匹配,一个点号就够
console.log(/^\u{1F600}$/u.test(emoji)) // true:u 模式下的码点转义
console.log(/^\u{1F600}$/.test(emoji)) // false:没有 u 时这不是码点转义查看输出与解释
2
false
true
true
falselength 是 2,说明字符串里确实存了两个单位:高代理项 \uD83D 和低代理项 \uDE00。不加 u 时,正则眼里就是两个字符,^.$ 要求只有一个字符,自然失败。
最后一行容易让人意外。没有 u 时,\u{1F600} 不会报错,而是被解释成字母 u 后面跟一个大括号内容,完全是另一个意思。所以写码点转义一定要带 u,不然正则悄悄匹配错东西,很难排查。
量词和字符类也会把 emoji 拆开
点号只是最显眼的一个。量词和字符类的问题在实际项目里更隐蔽,比如表情白名单、限制连续表情个数这类需求。
console.log(/^😀{2}$/.test('😀😀')) // false:{2} 只作用在低代理项上
console.log(/^😀{2}$/u.test('😀😀')) // true
console.log(/^[😀]$/.test('😀')) // false:字符类里其实是两个字符
console.log(/^[😀]$/u.test('😀')) // true
try {
new RegExp('[😀-😂]') // 范围两端都被拆开,变成非法范围
} catch (e) {
console.log(e.name) // SyntaxError
}
console.log(new RegExp('[😀-😂]', 'u').test('😁')) // true查看输出与解释
false
true
false
true
SyntaxError
true量词只修饰它前面的一个原子。不加 u 时,😀{2} 实际上是「高代理项 + 低代理项重复两次」,所以 😀😀 匹配不上。
字符类同理,[😀] 在非 u 模式下等于「高代理项或低代理项」,它能匹配半个 emoji,却匹配不了完整的一个。写成范围 [😀-😂] 更糟:范围变成从低代理项 \uDE00 到高代理项 \uD83D,起点比终点大,构造正则时直接抛 SyntaxError。
- 用
\p{...}按类别匹配 - 有了
u,就能用 Unicode 属性转义按类别匹配,比如\p{Letter}匹配任何语言的字母,\p{Emoji_Presentation}匹配默认以 emoji 样式显示的字符。这比手写一长串码点范围可靠得多。不加u时,\p{Letter}会被当成字母p加大括号文本,不会报错,但也不是你想要的意思。
u 模式为什么更严格
u 除了改变匹配单位,还会关掉一批历史遗留的宽松语法。以前的正则允许各种「没意义的转义」,比如 /\a/ 等同于 /a/,/\-/ 在字符类外也能用。开启 u 之后,这些写法全部是 SyntaxError,只有真正有意义的字符才能被转义。
这么做的原因是为了给新语法留位置。\u{...}、\p{...} 这些写法在旧模式下已经有了「宽松」的含义,没法改,所以只能在 u 模式下重新定义,并且要求其他转义也写得规范,避免歧义。
所以给老正则批量加 u 不是零风险的。多余的反斜杠、字符类里不合规的写法,加完之后会在构造时报错。好在它是构造阶段就报错,跑一遍用例就能发现。
容易答错的地方
- 以为加了
u字符串length就变了 u是正则的标志,只影响正则怎么读字符串,字符串本身没有任何变化。'😀'.length还是 2。想按码点数个数,用[...str].length。- 以为
u能处理所有 emoji u保证的是一个码点不被拆开。但 👨👩👧 这种家庭 emoji 是三个人的码点用零宽连接符串起来的,一共 5 个码点。带肤色的 emoji、国旗也都是多个码点拼的。/^.$/u同样匹配不了它们。要按用户看到的「一个字」处理,得用Intl.Segmenter的grapheme粒度。- 以为
\u{...}不加u会报错提醒你 - 不会报错,它会被静默解释成别的意思,这比报错更危险。看到代码里有
\u{或\p{,先检查正则是不是带了u。
面试官还会怎么问?
v 标志和 u 是什么关系?
v 是 ES2024 加入的,可以看作 u 的升级版,两者不能同时使用。它在 u 的基础上让字符类支持交集和差集,比如 [\p{Letter}--[a-z]],还能匹配由多个码点组成的属性,比如 \p{RGI_Emoji} 能匹配整个家庭 emoji。新代码如果运行环境支持,可以考虑用 v。
加了 u 之后,lastIndex 会停在代理对中间吗?
不会。比如 const re = /./gu,手动把 re.lastIndex 设成 1,也就是 😀 的低代理项位置,re.exec('😀') 依然返回完整的 😀。同样的操作换成 /./g,返回的是孤零零的低代理项 \uDE00。空匹配推进位置时也一样,u 模式按码点前进,所以 'a😀b'.split(/(?:)/u) 得到 3 项,'a😀b'.split('') 得到 4 项。
带 i 的大小写不敏感匹配,加 u 有区别吗?
有。加了 u 之后,大小写折叠使用 Unicode 的完整规则。比如开尔文符号 \u212A(长得和大写 K 一样)在 /k/iu 下能匹配,在 /k/i 下不能。处理多语言文本时,这个差别可能导致搜索结果不一致。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。