
把一批中文姓名交给默认排序,陈六落在最后;给比较器指定中文区域后,它到了最前面。可同一把中文排序器遇到「第10章」和「第2章」,10 仍排在 2 前。区域规则解决了汉字怎么比较,并不会顺手把字符串里的数字变成数值。
这组结果来自 Node.js v26.8.1,运行时使用 full-icu,默认 locale 是 en-US,系统 locale 相关环境变量未设置。以下排序与耗时都对应 tools/verify-intl-collator.mjs 的本机输出,不是对所有运行时的保证。
姓名顺序先看比较规则
脚本把同一批 15 个姓名分别交给六种比较器。默认比较与不带参数的 localeCompare() 输出完全相同;中文 locale 把陈、何、李、吕排到前段;笔画排序又是另一份名单。
| 比较方式 | 实测开头 | 实测结尾 |
|---|---|---|
| 默认比较 | 何四 / 吕五 / 吴十 | 郑一 / 陈六 |
localeCompare('zh') |
陈六 / 何四 / 李四 | 赵七 / 郑一 / 周九 |
Collator('zh-Hans-CN') |
陈六 / 何四 / 李四 | 赵七 / 郑一 / 周九 |
Collator('zh-Hans-CN-u-co-stroke') |
王五 / 吕五 / 孙八 | 赵七 / 郑一 / 秦二 |
Collator('en') |
何四 / 吕五 / 吴十 | 郑一 / 陈六 |
node 版本: v26.8.1
ICU: full-icu
默认 locale: en-US
Collator('zh-Hans-CN'): 陈六 何四 李四 吕五 欧阳修 秦二 施六 孙八 王五 吴十 许三 张三 赵七 郑一 周九
Collator('zh-Hans-CN-u-co-stroke'): 王五 吕五 孙八 许三 何四 吴十 张三 李四 周九 欧阳修 陈六 施六 赵七 郑一 秦二
zh 和 zh-Hans-CN 在这批数据上给出相同顺序;追加 u-co-stroke 后则变成笔画序。这里不能把「中文排序」理解成唯一固定的名单:排序器采用哪个 locale 和 collation,直接改变比较结果。默认环境恰好是 en-US 时,不带参数的比较也不会替应用猜测它想要中文拼音顺序。
const collator = new Intl.Collator('zh-Hans-CN');
const sortedNames = names.toSorted(collator.compare);
比较器适合按用途建一次再复用。实际项目的姓名顺序还受产品选定的排序约定影响;如果目标是按笔画,而代码写的是默认中文 collation,输出虽稳定,也可能不是产品要的规则。
中文 locale 不会开启数字排序
章节号的对照更直观:
| 比较方式 | 输出顺序 |
|---|---|
| 输入 | 第2章 / 第10章 / 第1章 / 第20章 / 第3章 |
| 默认比较 | 第10章 / 第1章 / 第20章 / 第2章 / 第3章 |
Collator('zh-Hans-CN') |
第10章 / 第1章 / 第20章 / 第2章 / 第3章 |
加 numeric: true |
第1章 / 第2章 / 第3章 / 第10章 / 第20章 |
默认比较: 第10章 第1章 第20章 第2章 第3章
Collator('zh-Hans-CN'): 第10章 第1章 第20章 第2章 第3章
Collator('zh-Hans-CN', {numeric:true}): 第1章 第2章 第3章 第10章 第20章
只指定 locale,默认结果仍是按字符串片段比较,所以第10章会跑到第2章前。要让连续数字作为数字比较,需要明确打开选项:
const chapterCollator = new Intl.Collator('zh-Hans-CN', {
numeric: true,
});
const chapters = ['第2章', '第10章', '第1章', '第20章', '第3章'];
console.log(chapters.toSorted(chapterCollator.compare));
// [ '第1章', '第2章', '第3章', '第10章', '第20章' ]
这也是最容易漏掉的配置差异:locale 决定语言相关的比较规则,numeric 则决定数字子串是否按数值顺序处理。文件名或章节号中出现数字片段时,单写 new Intl.Collator('zh-Hans-CN') 并不等于自然排序。反过来,编号如果包含业务定义的前导零、特殊分隔符或非十进制规则,也不能只凭 numeric 推断整个业务排序已经正确。比如「第02章」与「第2章」究竟要不要视作相邻甚至相等,是产品对编号的定义,不应由排序器替产品做决定。数据里若混入空字符串或缺失字段,也应在比较之前决定如何处理;让排序器直接比较未规范化的业务值,只会把未定义的次序藏进列表。
灵敏度不会把不同字变成相等
脚本还比较了「妈、麻、马、骂」。默认字符串比较的结果是「妈、马、骂、麻」,Collator('zh') 则是「妈、麻、马、骂」。设为 sensitivity: 'base' 后,本次输出仍是后一个顺序;'妈'.localeCompare('麻', 'zh', { sensitivity: 'base' }) 的结果仍为 -1。
因此,不能只看到 base 就断定声调差异已被抹平,或同音字会相等。排序器返回负数、零、正数表达的是先后关系,不是拼音键,也不意味着字符串被归一化。若业务要把同音姓名放进同一组,或要稳定地按自定义拼音键排序,需要另行生成业务排序键;不能把比较结果当成键值存起来。
localeCompare() 和 Intl.Collator 在这里都是为排序提供比较结果。需要大量比较时,复用 Intl.Collator 可以避免在比较回调里反复构造实例。脚本对 5 万个三字字符串排了一遍,每种写法各测三次:每个元素新建 Collator 是 1113、1083、1099 毫秒;无参数 localeCompare 是 22 毫秒;复用同一 Collator 是 18 毫秒。另一次 2 万条测试分别耗时 9 毫秒和 7 毫秒,日志给出的比值为 1.28 倍。日志中的原始计时如下:
每个元素 new Collator: 1113ms / 1083ms / 1099ms
localeCompare 无参数: 22ms / 22ms / 22ms
复用同一个 Collator: 18ms / 18ms / 18ms
localeCompare 无参数: 9ms
复用 Collator: 7ms
倍数: 1.28x
这只是这台机器上这份脚本的耗时,不是性能承诺;不能据此说所有数据集、引擎或 locale 都会有相同比值。我会把排序器放在重复调用的比较路径之外,主要是避免不必要的实例创建,是否有明显收益仍应按实际数据量测量。
姓名列表要中文顺序时,明确指定目标 locale,并确认产品采用拼音还是笔画等规则;章节号要自然数字顺序时,再设置 numeric: true。如果比较规则与业务要求一致,复用一个 Collator 就够了;若顺序涉及自定义优先级或特殊编号,先定义明确的排序键,不要指望 locale 自动理解业务含义。