ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

JavaScript实现汉字按拼音排序的完整方案与避坑指南

JavaScript实现汉字按拼音排序的完整方案与避坑指南 做前端这几年凡是跟“排序”沾边的需求十有八九会碰到中文按拼音排序的问题。通讯录要按姓名首字母分组城市选择器要把“重庆”排在C组而不是Z组商品列表要按名称的拼音顺序展示。每次遇到这种需求总有同事第一反应是直接调sort()结果一跑发现“北京”排在“上海”后面直接傻眼。这个标题看着简单真做起来坑比想象中多。今天就把 JavaScript 里汉字按拼音 a-z 排序这件事彻底讲透从原生 API 的用法到多音字的坑从数组排序到通讯录分组全是我实际踩过坑之后沉淀下来的方案可以直接抄作业。1. 需求拆解与实现思路1.1 汉字排序的难点在哪里首先要搞清楚一个问题为什么 JS 原生的排序不能直接用JavaScript 的Array.prototype.sort()默认把元素转成字符串然后按 UTF-16 编码单元的数值大小排序。问题在于汉字在 Unicode 里的编码顺序是按照部首和笔画来的跟拼音没有任何关系。举个例子“爱”的 Unicode 码点是\u7231“北”是\u5317“城”是\u57CE。默认sort()排序时“北”(0x5317) 排在“城”(0x57CE) 前面“城”又排在“爱”(0x7231) 前面。但按拼音应该是“爱”(ai) → “北”(bei) → “城”(cheng)顺序完全乱套。这背后涉及的机制是 ICUInternational Components for Unicode。现代浏览器和 Node.js 在实现localeCompare和Intl.Collator时会调用 ICU 内置的语言排序规则。中文排序规则里面包含了拼音字典数据所以它知道“爱”对应的拼音是“ai”“北”是“bei”。但前提是你要把这个语言环境参数正确传进去。1.2 方案选型原生 API 还是拼音库我梳理了当前主流的三种方案先做个对比方案实现方式优点缺点A字符串调用localeCompare(b, zh-Hans-CN)原生支持无需依赖性能好多音字受限于 ICU 字典个别字不准BIntl.Collator实例化比较器可复用实例适合高频比较参数更精细底层逻辑跟 A 一样多音字问题同样存在C引入 pinyin / pinyin-pro 拼音库能拿到完整拼音和首字母控制力最强增加包体积大列表场景需要做缓存我的习惯是如果只是排序优先用方案 A 或 B如果需要拿拼音做更多事情搜索、分组、首字母索引再引入拼音库。为了一个排序功能就甩一个几百 KB 的库进去完全不划算。另外说一句别迷信哪种方案一定“最正确”。我见过有人只凭一两个用例就断定localeCompare不行结果换成拼音库又得处理各种多音字词库反而更麻烦。选型的核心依据是业务场景对准确率的要求以及你手里已有的依赖。2. localeCompare 与 Intl.Collator 核心用法2.1 基础语法和参数拆解localeCompare的完整语法是string.localeCompare(compareString, locales, options)三个参数里locales和options都可以省略。但做中文拼音排序时这两个参数恰恰是关键。先看locales。要传中文语言标签const arr [重庆, 北京, 上海, 深圳, 广州]; arr.sort((a, b) a.localeCompare(b, zh-Hans-CN)); console.log(arr); // 期望结果[北京, 重庆, 广州, 上海, 深圳]这里有几个常见的语言标签写法zh、zh-CN、zh-Hans-CN、zh-Hant-TW。在大多数现代浏览器里简体中文的排序结果差异不大。但有一个细节我测试过如果你不传或只传zh某些浏览器的默认 ICU 数据可能拿不到简体中文的排序规则结果退化到按 Unicode 码点排序。所以稳妥的做法是写zh-Hans-CN这种完整的 BCP 47 标签。再来看options。这个参数控制比较精度最常用的几个选项const collator new Intl.Collator(zh-Hans-CN, { sensitivity: base, numeric: true, ignorePunctuation: true });sensitivity: base忽略大小写、重音符号和声调只区分字母本身。做拼音排序时最推荐这个。numeric: true排序时把数字按数值比较而不是字符串字典序。这样“第2章”会排在“第10章”前面。ignorePunctuation: true忽略标点符号对排序的影响。2.2 两种 API 的差异与性能对比localeCompare和Intl.Collator底层是同一套 ICU 排序规则功能上等价。区别在于使用方式// 不推荐每次比较都走一遍内部逻辑 arr.sort((a, b) a.localeCompare(b, zh-Hans-CN)); // 推荐先创建比较器复用 compare 方法 const collator new Intl.Collator(zh-Hans-CN, { sensitivity: base }); arr.sort(collator.compare);Intl.Collator实例化一次后compare方法可以直接复用。对于反复排序的场景比如用户点了好几次表头切换升降序性能差距会很明显。我实际测过一万条数据的数组复用 Collator 比每次调用localeCompare快约 30%~40%。另外提醒一句collator.compare是一个函数引用直接传给sort没问题。但要注意箭头函数的写法会让this绑定失效这个在比较器里通常不影响因为compare不依赖this。2.3 关于排序稳定性ES2019 之后Array.prototype.sort被要求是稳定排序。也就是说如果两个元素的比较结果相同它们原来的相对顺序会被保留。这一点对中文排序挺重要。比如你有一个列表先按创建时间排好再按拼音排序。当拼音相同时稳定排序能保留之前的时间顺序用户看到的就是“拼音相同再按时间排”的效果。老一些的浏览器比如 IE11 和某些旧版 WebView不是稳定排序如果你需要兼容它们就得自己加一个序号字段作为第二比较键。users.sort((a, b) { const result collator.compare(a.name, b.name); if (result ! 0) return result; return a.createTime - b.createTime; // 第二排序键 });3. 完整项目实现从数组到通讯录分组3.1 纯字符串数组排序最基础的场景直接把城市列表按拼音排序const cities [重庆, 北京, 上海, 深圳, 广州, 成都, 杭州]; const collator new Intl.Collator(zh-Hans-CN, { sensitivity: base }); cities.sort(collator.compare); console.log(cities); // [北京, 成都, 重庆, 广州, 杭州, 上海, 深圳]注意观察“重庆”的位置它在“成都”之后、“广州”之前说明“重”确实被识别为 chong而不是 zhong。这是大多数现代浏览器中 ICU 字典的表现但我在某些旧版 Android WebView 里测过“重庆”会被排到 Z 组去。如果你的用户群里有大量旧设备最好用我后面说的“拼音库自定义词典”兜底方案。3.2 对象数组按拼音字段排序实际业务中更多是对象数组比如人员列表按姓名排序const users [ { name: 张三, age: 28 }, { name: 李四, age: 32 }, { name: 王五, age: 25 }, { name: 陈六, age: 30 } ]; const collator new Intl.Collator(zh-Hans-CN, { sensitivity: base }); users.sort((a, b) collator.compare(a.name, b.name)); // 按 陈、李、王、张 的顺序排列这里有一个必须处理的情况name字段可能为空或undefined。不处理的话collator.compare会直接抛异常。我的习惯是先做空值判断users.sort((a, b) { const nameA a.name ?? ; const nameB b.name ?? ; return collator.compare(nameA, nameB); });空字符串的排序位置在最前面符合大多数业务直觉。3.3 通讯录首字母分组实战“按首字母分组”和“按拼音排序”是一对孪生需求通讯录、城市索引都用得到。这里需要拿到每个汉字的首字母原生Intl.Collator只能比较排序不能输出拼音字符串所以我会引入 pinyin-pro 这个库。先安装npm install pinyin-pro然后取首字母并分组import { pinyin } from pinyin-pro; function getInitial(char) { const result pinyin(char, { pattern: first, toneType: none }); return result.charAt(0).toUpperCase(); } const contacts [ { name: 张三 }, { name: 李四 }, { name: 王五 }, { name: 陈六 }, { name: Ann } ]; // 先按拼音排序 const collator new Intl.Collator(zh-Hans-CN, { sensitivity: base }); contacts.sort((a, b) collator.compare(a.name, b.name)); // 再按首字母分组 const groups {}; contacts.forEach(item { const initial getInitial(item.name.charAt(0)); if (!groups[initial]) { groups[initial] []; } groups[initial].push(item); }); console.log(groups); // A: [{ name: Ann }] // C: [{ name: 陈六 }] // L: [{ name: 李四 }] // W: [{ name: 王五 }] // Z: [{ name: 张三 }]关于拼音库选型我对比过pinyin、pinyin-pro、tiny-pinyin这几个pinyin-pro体积小gzip 后 8KB 左右多音字识别率高支持自定义词典推荐。pinyin老牌库功能全但体积偏大。tiny-pinyin极简但不支持多音字适合只取首字母的场景。3.4 点击表头排序的表格实现管理后台的表格基本都有“点击表头排序”的功能放在这里一起讲因为核心逻辑完全一样只是多了方向切换和状态管理。function sortTableByField(tableData, field, order) { const collator new Intl.Collator(zh-Hans-CN, { sensitivity: base }); return [...tableData].sort((a, b) { const result collator.compare(String(a[field] ?? ), String(b[field] ?? )); return order asc ? result : -result; }); }这里有一个经验之谈排序前一定要先复制数组用[...tableData]而不是直接tableData.sort(...)。尤其在 React/Vue 这种数据驱动视图的框架里直接修改原数组会导致你在setState或reactive里很难追踪数据变化容易触发重复渲染、列表 key 错乱等一堆问题。很多搜索词里出现的“点击表头排序”“字符串排序”“js 忽略大小写”都是在这个场景下衍生的。实际项目里涉及中英文混排时sensitivity: base就能同时解决忽略大小写的问题。4. 多音字、生僻字与边界场景处理4.1 多音字为什么不能完美解决这是汉字拼音排序最让人头疼的部分。同一个字在不同词语里读音可能完全不同而 ICU 内置的排序规则只能根据字本身的常见读音来排。举几个典型例子“重庆”的“重”读 chóng但默认字典可能按 zhòng 处理“厦门”的“厦”读 xià但默认可能按 shà 处理“单”作姓氏读 shàn作形容词读 dān“解”作姓氏读 xiè我实测过 Chrome 最新版“重庆”能正确排到 C 组但“单田芳”“解晓东”这类人名的姓氏读音就很不稳定。这不是 JS 的 bug而是 ICU 字典覆盖不了所有上下文。4.2 拼音库兜底与自定义词典如果你的业务对多音字准确率有硬性要求我推荐一个三层策略自定义词典优先把业务里高频出现的特殊读音词条维护成一个 JSON排序前先查这个词条。拼音库兜底没命中词典的词用 pinyin-pro 这种带多音字词库的库生成拼音。原生 API 保底如果用户环境不支持 ES Module 或者不想引库退回到localeCompare。pinyin-pro 支持传入自定义拼音import { pinyin } from pinyin-pro; const customDict { 重庆: chong qing, 厦门: xia men, 单田芳: shan tian fang, 解晓东: xie xiao dong }; function sortWithDict(arr) { return arr.sort((a, b) { const pa customDict[a] || pinyin(a, { toneType: none, type: array }).join( ); const pb customDict[b] || pinyin(b, { toneType: none, type: array }).join( ); return pa.localeCompare(pb, zh-Hans-CN, { sensitivity: base }); }); }这个方法的好处是词典只管特殊词常见词交给拼音库整体可控性很强。坏处也很明显需要一个维护成本。我会把这个词典放到后端接口里下发这样前端不用发版就能更新读音数据。4.3 中英文混合、数字开头与特殊字符中文排序场景里经常混着英文、数字和标点这类边角情况需要单独处理。我的经验是先按“首字符类型”分桶再做组内排序。比如约定排序规则为数字 英文 中文或者反过来。不然的话“123”和“abc”和“张三”混在一起比较很容易出现预期之外的顺序。function mixedSort(arr) { const collator new Intl.Collator(zh-Hans-CN, { sensitivity: base, numeric: true }); return [...arr].sort((a, b) { const typeA getType(a); const typeB getType(b); if (typeA ! typeB) return typeA - typeB; return collator.compare(a, b); }); } function getType(str) { const first str.charAt(0); if (/[0-9]/.test(first)) return 0; if (/[a-zA-Z]/.test(first)) return 1; return 2; }这个“先分桶再排序”的思路比在比较函数里写一堆正则判断要清晰得多也好维护。5. 常见问题与排查技巧实录5.1 大小写和音调干扰排序现象apple和Apple被排到完全不同的位置或者“妈”(mā) 和“马”(mǎ) 被当成不同字处理。原因localeCompare默认的分级是variant会区分大小写、重音和音调。拼音排序的预期通常不关心声调。解决设置sensitivity: base。const collator new Intl.Collator(zh-Hans-CN, { sensitivity: base });这个选项我在多个项目里都是固定配置省心。5.2 不同浏览器与 Node 版本的结果差异最让人无语的坑同一份代码Chrome 里排序正常Firefox 里顺序不一样Node 里又是第三种结果。根源是 ICU 数据版本不一致。浏览器各自内置的 ICU 完整度不同尤其老版本 Safari 和某些国产浏览器中文排序支持非常薄弱。Node.js 环境更明显。Node 14 之前的默认构建只带small-icu只包含很少的语言数据中文排序大概率直接用不了。我踩过这个坑之后排查步骤固定如下直接在 Node 里跑一次北京.localeCompare(上海, zh-Hans-CN)看结果是不是负数。如果是 0相等说明 ICU 数据不完整考虑升级 Node或者用full-icu包补全数据。如果业务必须跨端保持顺序一致最稳妥的办法是服务端返回拼音字段前端只做字段字符串比较。5.3 大列表性能优化一万条数据的数组排序Intl.Collator大概几十毫秒看起来不慢。但如果每条数据里还要做拼音转换比如为了首字母分组性能就会明显劣化。优化方案是缓存。拼音转换的结果是确定性的同一个字永远转出同一个拼音。用Map做一层缓存const pinyinCache new Map(); function getPinyinWithCache(text) { if (pinyinCache.has(text)) { return pinyinCache.get(text); } const result pinyin(text, { toneType: none, type: array }).join( ); pinyinCache.set(text, result); return result; }实务里这个缓存效果立竿见影。通讯录这种重复姓氏很多的场景缓存命中率能到 95% 以上排序耗时基本可以忽略。5.4 排序结果和接口对不上有一种很常见的场景前端自己排了一版后端接口也有一个排序字段两边结果不一致产品经理来问。我遇到过几次最后定位到的原因都是前端的排序规则和后端的排序规则不是同一套。比如后端用的是 MySQL 的ORDER BY CONVERT(name USING gbk)按 GBK 编码顺序排而前端用的是拼音两边的“字典序”概念根本不一样。这种事靠前端调代码是调不齐的。正确做法是排序规则以一端为准。要么后端把所有数据排好再返回前端只做展示要么后端把每个字段的拼音串作为独立字段返回前端直接用String.prototype.localeCompare做普通字符串比较。两边都严格走同一条拼音数据链路才能保证一致。6. 写在最后方向比代码更重要做前端这几年我最大的感受是像“汉字按拼音排序”这种需求真正难的往往不是那一行sort代码而是你选哪条路走下去。如果你只做一次简单的城市列表排序原生Intl.Collator一行就够。如果你做的是通讯录、企业组织架构、后台表格这种对准确率和性能都有要求的场景别犹豫直接上“拼音库自定义词典缓存”的组合。要记住一个原则方案复杂度永远跟着业务需求走不要为了技术炫耀引入复杂度也不要为了省事而牺牲准确率。最后分享一个排查排序问题的小工具。我会在控制台里跑这样一段代码快速看当前运行环境对中文排序的支持情况console.log(北京.localeCompare(上海, zh-Hans-CN)); // 期望输出负数因为 bei shang如果输出 0 或正数说明环境有问题这个简单的验证步骤能帮你省掉后面排查环境的非常多时间。排序看起来是小事但踩过坑的人都知道它有时候比写一个业务模块还磨人。希望这篇文章能让你少走几步弯路。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表