先记住这个答案
Array.prototype.sort() 不传比较函数时,会把元素转成字符串,按 UTF-16 编码逐个字符比较。编码顺序和人的阅读习惯是两回事:中文的编码顺序不是拼音顺序,'item10' 因为第五个字符 '1' 小于 '2',排在了 'item2' 前面。按语言习惯排序要用 Intl.Collator:先 new Intl.Collator('zh-CN'),再把它的 compare 方法传给 sort()。需要数字按数值比较时加 numeric: true,想忽略大小写和重音时设置 sensitivity。
- 默认
sort()按 UTF-16 编码比较,不按字典顺序 Intl.Collator('zh-CN')让中文按拼音排序numeric: true让item2排在item10前面- 大数组排序先建好 Collator 再复用
中文按拼音、编号按数值
后台管理页面最常见的两个排序需求:用户名按拼音排,文件名里的编号按数值大小排。默认的 sort() 两个都做不到。
const names = ['张三', '李四', '王五', '阿明']
console.log([...names].sort().join(' ')) // 按编码
console.log([...names].sort(new Intl.Collator('zh-CN').compare).join(' ')) // 按拼音
const files = ['item10', 'item2', 'Item1']
console.log([...files].sort().join(' ')) // 大写字母编码更小,排最前
const byNumber = new Intl.Collator('en', { numeric: true })
console.log([...files].sort(byNumber.compare).join(' '))查看输出与解释
张三 李四 王五 阿明
阿明 李四 王五 张三
Item1 item10 item2
Item1 item2 item10第一行碰巧和原数组顺序一样,只是因为这几个字的编码刚好是这个顺序,和拼音无关。用 zh-CN 的 Collator 之后,阿(a)、李(li)、王(wang)、张(zhang)按拼音排好了。
numeric: true 会把字符串里连续的数字当成一个整数来比,所以 2 排在 10 前面。文件名、版本号、订单编号这类混着数字的字符串,基本都需要这个选项。
compare 可以直接传给 sort(),因为 Collator 实例上的 compare 是已经绑定好的函数,不会丢 this。
同一对字符,换个语言结果就不同
Collator 的规则来自语言本身,不是一张全世界通用的表。另外,「两个字符串算不算相等」也可以调。
locale 和 sensitivity 的影响JavaScriptconst letters = ['z', 'ä', 'a']
console.log(letters.sort(new Intl.Collator('de').compare).join(' ')) // 德语
console.log(letters.sort(new Intl.Collator('sv').compare).join(' ')) // 瑞典语
const base = new Intl.Collator('en', { sensitivity: 'base' })
console.log(base.compare('a', 'A'), base.compare('e', 'é')) // 大小写、重音都不算差别
const accent = new Intl.Collator('en', { sensitivity: 'accent' })
console.log(accent.compare('a', 'A'), accent.compare('e', 'é')) // 只有重音算差别查看输出与解释
a ä z
a z ä
0 0
0 -1德语把 ä 当成 a 的变体,排在 a 旁边;瑞典语里 ä 是一个独立字母,排在 z 后面。所以 locale 一定要按用户的语言传,别让它用运行环境的默认值。
sensitivity 有四档:base 只看基础字母,a、A、á 都算一样;accent 还看重音;case 还看大小写;variant 全都区分。做搜索框的模糊匹配时常用 base,用户输入 resume 也能匹配到 Résumé。
- 为什么要把 Collator 提到循环外面
- 每次
new Intl.Collator()都要加载和解析语言数据,开销不小。在sort()的比较函数里写a.localeCompare(b, 'zh-CN'),排一万条数据会触发几十万次比较,每次都可能重新准备一遍规则。先建一个 Collator,再复用它的compare,是 MDN 也推荐的写法。
容易答错的地方
- 以为
compare一定返回 -1 或 1 - 规范只保证负数、0、正数。比较函数应该直接返回它的结果,判断先后时写
< 0,不要写=== -1。 - 不传
locale还指望各端结果一致 - 不传时用运行环境的默认语言,服务端 Node 可能是
en-US,用户浏览器是zh-CN,同一份数据排出两种顺序。分页列表在服务端排序、客户端又排一次,还会出现数据跳动。需要稳定结果就显式传语言。 - 用
sensitivity: 'base'判断唯一性 base下'Admin'和'admin'比较结果是 0。拿它做排序、搜索没问题,拿它判断用户名是否重复就会出问题。唯一性校验应该用明确的规范化规则,比如统一转小写再比较。
面试官还会怎么问?
localeCompare 和 Intl.Collator 怎么选?
a.localeCompare(b, locale, options) 的参数和 Collator 一样,结果也一样。偶尔比较一两次用 localeCompare 更顺手;排大数组、或者同一套选项要用很多次时,先建 Collator 再复用,性能更好。
多音字的拼音排序准吗?
不完全准。Collator 按字的常用读音排序,「重庆」的「重」可能按 zhòng 排。通讯录这类对拼音要求严格的场景,通常要借助拼音库先转出拼音,再按拼音字符串排序。
怎么让 A 和 a 排在一起,但大写排前面?
用 caseFirst: 'upper' 选项。默认情况下 Collator 已经把 A 和 a 排在一起,caseFirst 决定两者之间谁在前,可选 upper、lower 和 false。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。