侧边栏壁纸
博主头像
一笑痕

人生若只如初见,
是可喜亦或者是可悲?

  • 累计撰写 160 篇文章
  • 累计收到 7 条评论

中文名单排出来不对,多半是排序器没指定 locale

2026-10-7 / 0 评论 / 6 阅读

中文名单排出来不对,多半是排序器没指定 locale

一份通讯录按姓氏排序,屏幕上出来的顺序看着不太对:陈在最前面,周跑到最后,中间那段像被人重新洗过。名字一个没少,就是整列的顺序和拼音对不上。把这批名字丢给 node 排一遍,先看到的是开销那一栏:5 万个名字排一次,按元素新建排序器要 1113 毫秒,复用同一个只要 18 毫秒。

六种比较器排同一批名字

== 0. 运行时与默认区域 ==
  node 版本: v26.8.1
  ICU: full-icu
  默认 locale: en-US
  系统 locale 相关环境: (未设置)

== 1. 同一批姓名,六种比较器的结果 ==
  默认比较(无比较器): 何四 吕五 吴十 周九 孙八 张三 施六 李四 欧阳修 王五 秦二 许三 赵七 郑一 陈六
  localeCompare 无参数: 何四 吕五 吴十 周九 孙八 张三 施六 李四 欧阳修 王五 秦二 许三 赵七 郑一 陈六
  localeCompare('zh'): 陈六 何四 李四 吕五 欧阳修 秦二 施六 孙八 王五 吴十 许三 张三 赵七 郑一 周九
  Collator('zh-Hans-CN'): 陈六 何四 李四 吕五 欧阳修 秦二 施六 孙八 王五 吴十 许三 张三 赵七 郑一 周九
  Collator('zh-Hans-CN-u-co-stroke'): 王五 吕五 孙八 许三 何四 吴十 张三 李四 周九 欧阳修 陈六 施六 赵七 郑一 秦二
  Collator('en'): 何四 吕五 吴十 周九 孙八 张三 施六 李四 欧阳修 王五 秦二 许三 赵七 郑一 陈六

本机默认区域是 en-US。不传参数的比较和默认比较给出的顺序完全一样,看着像按字符编码值走。换成 zh 或者 zh-Hans-CN 之后,顺序变成拼音:陈、何、李、吕、欧阳排在前面,周落在最后。把排序方式换成笔画,同一批名字又是另一套顺序。

三个顺序并排看,同一个姓的位置能差好几个身位。通讯录那列不对,是因为默认区域里没有中文的排序规则,代码又没有指定。

第 10 章排在第 1 章前面

== 2. 数字串的默认顺序与 numeric 选项 ==
  默认比较: 第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章

数字串是按字符比的。第 1 章和第 10 章的第一个字符都是「第」,第二个字符都是 1,接着比第三个字符,0 的编码值比「章」小,于是第 10 章被排到前面。加上 numeric 之后,串里的数字段被当成数来比,顺序回到 1、2、3、10、20。

文件列表,版本号,章节号,这几类数据都会踩到。光指定 zh 不够,还得把这个开关一起打开。

四个同音字在两种规则下的位置

== 4. 同音不同调的四个字:默认比较按码位,zh 规则按声调 ==
  默认比较: 妈 马 骂 麻
  Collator('zh') 默认 sensitivity: 妈 麻 马 骂
  Collator('zh', {sensitivity:'base'}): 妈 麻 马 骂
  '妈'.localeCompare('麻','zh') = -1
  '妈'.localeCompare('麻','zh',{sensitivity:'base'}) = -1
  '张'.localeCompare('李','zh') = 1 (正数=李在前,拼音 li 排在 zhang 前面)

默认比较给出的顺序是妈、马、骂、麻,zh 规则给出的是妈、麻、马、骂。同一个音的四个字,一个按编码值排,一个按声调排,出来的两个序列没有一处重合。

把敏感度降到 base,也没能把它们并成一个:比较结果照样是 -1。想按首字母分组、把同音字当成同一类,这条路走不通,得自己把声调剥掉再比。

那 60 倍是从哪来的

== 5. 开销:5 万条排一遍,三种写法各测三次 ==
  每个元素 new Collator: 1113ms / 1083ms / 1099ms
  localeCompare 无参数: 22ms / 22ms / 22ms
  复用同一个 Collator: 18ms / 18ms / 18ms

== 6. 数据量放大一档:2 万条,看比值是否稳定 ==
  localeCompare 无参数: 9ms
  复用 Collator: 7ms
  倍数: 1.28x

排序过程会把每一对元素都比一遍,比较器建在排序回调里面,等于每个元素都新建一次排序器。5 万条数据,这一项就是 1113 毫秒,比复用同一个慢 60 倍。

数据量掉到 2 万条,复用同一个排序器与不传参数的比较只差 1.28 倍。这个差距不算大,说明两件事的花费来源不一样:不传参数的比较每次都要去查一遍区域规则,复用排序器把这部分省下来;而按元素新建排序器,省下的是另一笔账。

落到代码里的做法

第一种是把排序器提成模块级常量,整个应用只建一次,比较函数直接引用它。这一改动的收益最大,也最省事。

第二种是数据量到几万条以上、或者排序结果要存下来的时候,先算好排序键。把名字转成拼音串存进列表项,之后每次排序比的都是普通字符串,规则清清楚楚,换语言也不用改排序逻辑。

第三种是把排序放到服务端或数据库里做,接口直接返回排好的顺序。代价是前端做本地筛选和分页时要留意顺序的权威在哪边。

判断要不要动排序这一层,有个很省事的办法:把数据里的姓名换成一批中文名试一次,如果结果和拼音对不上,问题就在区域参数这一层,不在数据结构上。

这次的取舍是把排序器提成模块级常量,中文键在数据入库前算好。这样同一份数据在列表页和导出文件里的顺序一致,出了问题只用看一个地方。我不建议在渲染函数里新建排序器,那个位置每帧都可能被调用到。

    🤞 分享