先记住这个答案
Unicode 允许同一个字符有不同的编码方式。比如 é 可以写成预组合码点 U+00E9,也可以写成字母 e 加一个组合重音符 U+0301,渲染出来完全一样。但 === 比较的是底层码元序列,一个长度是 1,一个长度是 2,结果自然是 false。normalize() 能把字符串转成统一的正规化形式,两边都转成 NFC 或都转成 NFD 后再比较,就能得到符合直觉的结果。
===比的是码元序列,不是显示效果normalize()默认转成 NFC,也就是尽量合并- NFD 拆开,NFC 合并,两边统一成一种再比
- K 系列会把全角、圈号数字等兼容字符也折叠掉
用 é 看一遍差异
下面两个字符串在页面上都显示成 café,把码点逐个打印出来就能看出区别。
const nfc = 'caf\u00E9' // é 是一个预组合码点 U+00E9
const nfd = 'cafe\u0301' // e + 组合重音符 U+0301,显示同样是 café
const hex = (s) => [...s].map((c) => c.codePointAt(0).toString(16)).join(' ')
console.log(hex(nfc), '|', hex(nfd))
console.log(nfc === nfd, nfc.length, nfd.length)
console.log(nfc.normalize() === nfd.normalize()) // 默认 NFC
console.log(nfd.normalize('NFC').length, nfc.normalize('NFD').length)
console.log('full'.normalize('NFKC'), '①'.normalize('NFKC'))查看输出与解释
63 61 66 e9 | 63 61 66 65 301
false 4 5
true
4 5
full 1normalize() 不传参数时用的是 'NFC',它会把能合并的组合合并成一个码点,所以 nfd 变成了长度 4。反过来 'NFD' 会把 é 拆成两个码点,长度变成 5。两边统一成哪一种都可以,关键是一样。
最后一行是 K 系列的效果:全角字母被折叠成普通字母,圈号数字 ① 变成了 1。
C、D 和 K 分别是什么意思
C 是 Composition(合成),D 是 Decomposition(分解)。NFC 和 NFD 属于「标准等价」,只处理同一个字符的不同写法,转换前后字符的含义和显示完全一样,不丢任何信息。
K 是 Compatibility(兼容)。NFKC 和 NFKD 除了标准等价,还会把「长得像、含义接近」的兼容字符也归一化:全角字母变半角,连字 fi 拆成 f 和 i,上标 ² 变成 2。这些字符显示上其实不一样,所以 K 系列是有损的,适合做搜索、比对,不适合改写用户数据。
实际会在哪里踩坑
最典型的是 macOS 文件名。macOS 的文件系统历史上会把文件名存成接近 NFD 的形式,从 Mac 上传的「café.txt」到服务端可能是分解形式,和数据库里用户手输的 NFC 形式对不上,查重、按文件名搜索都会失败。上传时对文件名统一做一次 normalize('NFC') 可以避开这个问题。
用户名、邮箱注册也是一样。不做正规化,两个视觉上相同的用户名可能都能注册成功,给冒名顶替留了空子。常见做法是入库前统一转 NFC,比较和唯一索引都基于正规化后的值。
搜索框的场景可以更激进一点:把输入和索引都转成 NFKC 再转小写,用户输全角「ABC」也能搜到「abc」。
容易答错的地方
- 以为
normalize能解决所有「看起来一样」 - 正规化只处理 Unicode 定义了等价关系的字符。拉丁字母 a 和西里尔字母 а 长得一样,但它们不是等价字符,任何正规化形式都不会把它们合并。防钓鱼域名这种「形似字符」问题要靠专门的混淆字符表。
- 以为
localeCompare也会严格区分 'caf\u00E9'.localeCompare('cafe\u0301')返回 0,因为按语言规则排序时本来就把两种写法视为相等。但它只适合排序,做相等判断、当对象的键、放进Set去重时,还是得先normalize。- 只正规化一边
- 只对用户输入调用
normalize,而数据库里的旧数据没处理,照样比不上。正规化要在数据进入系统的入口统一做,比较时两边形式一致才有意义。
面试官还会怎么问?
normalize 传了不认识的参数会怎样?
只接受 'NFC'、'NFD'、'NFKC'、'NFKD' 四个值,传其它字符串会抛 RangeError。不传参数等同于 'NFC'。
正规化之后 length 一定变短吗?
不一定。NFC 通常会让长度变短或不变,NFD 会让含重音的字符串变长。还有一些字符在 NFKC 下会展开成多个字符,比如连字 fi 变成两个字母,长度反而增加。
对象的键和 Map 的键会自动正规化吗?
不会。属性名和 Map 键都按码元精确匹配,NFC 和 NFD 形式的 café 会被当成两个不同的键。用字符串做键前,需要自己统一正规化。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。