前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
首页程序员面试题库Unicode 正规化 normalize 字符串相等
JavaScript值与类型

两个看起来一模一样的字符串,为什么用 === 比较却不相等?

同一个字符在 Unicode 里可能有多种码点组合,比如 é 既可以是 U+00E9 一个码点,也可以是 e 加组合重音符 U+0301。=== 逐个比较码元,所以两者不相等。比较前先都调用 normalize() 转成同一种形式就行。

前端进阶之旅 · 一题精讲更新于 2026.09.29
JavaScript#值与类型
先看核心答案读代码示例
理解线索

四种正规化形式

  1. NFC先拆再合,尽量用预组合码点,normalize() 的默认值
  2. NFD全部拆成基础字符加组合符,é 变成两个码点
  3. NFKC / NFKD在上面基础上再做兼容折叠,全角字母变半角,① 变 1

NFKC 会丢信息,比如上标 ² 会变成普通的 2。用它做搜索匹配没问题,但不要把折叠后的结果写回存储,那会改掉用户原本的内容。

核心回答

先记住这个答案

Unicode 允许同一个字符有不同的编码方式。比如 é 可以写成预组合码点 U+00E9,也可以写成字母 e 加一个组合重音符 U+0301,渲染出来完全一样。但 === 比较的是底层码元序列,一个长度是 1,一个长度是 2,结果自然是 false。normalize() 能把字符串转成统一的正规化形式,两边都转成 NFC 或都转成 NFD 后再比较,就能得到符合直觉的结果。

  • === 比的是码元序列,不是显示效果
  • normalize() 默认转成 NFC,也就是尽量合并
  • NFD 拆开,NFC 合并,两边统一成一种再比
  • K 系列会把全角、圈号数字等兼容字符也折叠掉

用 é 看一遍差异

下面两个字符串在页面上都显示成 café,把码点逐个打印出来就能看出区别。

比较前先统一形式JavaScript
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 1

normalize() 不传参数时用的是 '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é 会被当成两个不同的键。用字符串做键前,需要自己统一正规化。

从一道题,走向一组知识

把知识连起来

值与类型

slice、substring、substr 遇到负数和起止颠倒时有什么区别?

NFD 形式下一个 é 占两个位置,slice 截断可能把重音符单独切掉。

值与类型

String 的 valueOf 和 toString 有区别吗?为什么对 new String 调用会拿到原始字符串?

两题都在问「字符串相等」背后比较的到底是什么,一个比码点,一个比包装对象和原始值。

参考资料

  • JavaScript:值与类型参考资料

示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。

本题目录
  1. 先记住这个答案
  2. 用 é 看一遍差异
  3. C、D 和 K 分别是什么意思
  4. 实际会在哪里踩坑
  5. 容易答错的地方
  6. 面试官还会怎么问
  7. 把知识连起来
读懂,再试着讲出来

先看核心答案,再读代码。最后展开追问,检查自己有没有遗漏边界。

试着回答追问
浏览全部面试题理解原理,也关注真实的使用场景。回到顶部 ↑