Intl.Collator API

实现跨语言字符串的正确排序与比较

简介

在 JavaScript 中排序字符串,看起来很简单,但一旦涉及国际化文本,事情就没那么容易了。默认的字符串比较基于 Unicode 码点值,这会导致很多语言得到错误的排序结果。Intl.Collator API 提供了遵循本地语言规则的字符串比较功能,能够正确处理特殊字符并尊重文化排序习惯。

默认排序为何失效

以排序德语名字列表为例:

const names = ["Zoe", "Ava", "Ärzte", "Änder"];
console.log(names.sort());
// ["Ava", "Zoe", "Änder", "Ärzte"]

这样的输出对德语使用者来说是错误的。在德语中,带元音变音符的字符比如 ä,应该排在其基础字母 a 附近,而不是放在最后。出现这种问题,是因为 JavaScript 按照 Unicode 码点值来比较字符串,其中 Ä (U+00C4) 排在 Z (U+005A) 之后。

不同语言有不同的排序规则。例如,瑞典语会把 ä 排在字母表末尾,德语会将其排在 a 附近,而法语则对有重音的字符有不同的处理。二进制比较无法反映这些文化差异。

字符串排序机制解析

排序(Collation)就是按照各自语言的规则,对字符串进行比较和排序的过程。Unicode 排序算法定义了如何通过分别分析字符、变音符号、大小写及标点来进行字符串比较。

比较两个字符串时,排序函数会返回一个数值:

  • 负值:第一个字符串排在第二个之前
  • 零:按当前敏感度级别,两字符串被视为等价
  • 正值:第一个字符串排在第二个之后

这种三向比较模式可以直接用于 Array.sort,并且让你能精准控制哪些差异影响排序。

使用 localeCompare 实现基础的本地化排序

localeCompare 方法可以实现本地化的字符串比较:

const names = ["Zoe", "Ava", "Ärzte", "Änder"];
console.log(names.sort((a, b) => a.localeCompare(b, "de")));
// ["Ava", "Änder", "Ärzte", "Zoe"]

这样就能得到正确的德语排序效果。第一个参数指定了区域,localeCompare 会自动遵循相应的文化规则。

你可以将选项作为第三个参数传递:

const items = ["File10", "File2", "File1"];
console.log(items.sort((a, b) =>
  a.localeCompare(b, "en", { numeric: true })
));
// ["File1", "File2", "File10"]

numeric 选项可以实现自然排序,使得 "2" 排在 "10" 之前。如果没有该选项,由于 "1" 排在 "2" 前,结果会是 "10" 排在 "2" 前。

重复调用 localeCompare 的性能问题

每次调用 localeCompare 都会从头处理一次区域设置。当需要对大型数组进行排序时,这会带来很大的性能开销:

// Inefficient: processes locale for every comparison
const sorted = items.sort((a, b) => a.localeCompare(b, "de"));

排序 1000 个元素大约需要比较 10000 次。每次比较都会重新创建一次区域配置,极大增加了性能成本。当界面中需要处理大量数据时,这种开销会非常明显。

利用 Intl.Collator 高效地进行字符串比较

Intl.Collator 会创建一个可复用的比较对象,它只需处理一次区域设置:

const collator = new Intl.Collator("de");
const sorted = items.sort((a, b) => collator.compare(a, b));

collator 实例会保存地区设置和比较规则。它的 compare 方法会在每次比较时直接使用这些预先计算好的规则,无需重复初始化。

对于大型数组排序,与反复调用 localeCompare 相比,性能可提升 60% 到 80%。

直接访问 compare 方法

你可以直接将 compare 方法传给 sort:

const collator = new Intl.Collator("de");
const sorted = items.sort(collator.compare);

这样做的原因是 compare 已经绑定在 collator 实例上。该方法会接收两个字符串并返回比较结果,正好符合 Array.sort 所期待的回调格式。

理解敏感度级别

sensitivity 选项用来控制字符串比较时哪些字符差异会被考虑在内。共有四个级别:

基础敏感度

基础敏感度会忽略重音符和大小写:

const collator = new Intl.Collator("en", { sensitivity: "base" });

console.log(collator.compare("a", "a")); // 0
console.log(collator.compare("a", "á")); // 0
console.log(collator.compare("a", "A")); // 0
console.log(collator.compare("a", "b")); // -1

只关注基本字母的不同。该级别适合模糊搜索场景,比如用户可能不会准确输入重音符时。

重音敏感度

重音敏感度会区分重音,但忽略大小写:

const collator = new Intl.Collator("en", { sensitivity: "accent" });

console.log(collator.compare("a", "a")); // 0
console.log(collator.compare("a", "á")); // -1
console.log(collator.compare("a", "A")); // 0
console.log(collator.compare("á", "A")); // 1

重音符与非重音符的字符是不同的,但同一个字母的大写与小写会被认为相同。

区分大小写

区分大小写时,大小写不同会被区分,但会忽略重音符:

const collator = new Intl.Collator("en", { sensitivity: "case" });

console.log(collator.compare("a", "a")); // 0
console.log(collator.compare("a", "á")); // 0
console.log(collator.compare("a", "A")); // -1
console.log(collator.compare("á", "Á")); // -1

会区分大小写,但忽略重音符。这种敏感度级别在实际中较少见。

区分字符变体

字符变体敏感会考虑所有差异:

const collator = new Intl.Collator("en", { sensitivity: "variant" });

console.log(collator.compare("a", "a")); // 0
console.log(collator.compare("a", "á")); // -1
console.log(collator.compare("a", "A")); // -1
console.log(collator.compare("á", "Á")); // -1

这是排序的默认方式。每个字符的不同都会产生不同的对比结果。

根据场景选择敏感度

不同的场景需要选择不同的敏感度:

  • 排序列表:使用字符变体敏感,确保严格排序
  • 内容搜索:使用基本敏感度,不区分重音符或大小写
  • 筛选选项:使用重音符敏感度,当大小写不重要时使用
  • 区分大小写的搜索:只需大小写敏感,忽略重音符

usage 选项为常见场景提供了默认的敏感度设置。

按用途优化排序和搜索模式

usage 选项可针对排序或搜索优化排序器的行为:

// Optimized for sorting
const sortCollator = new Intl.Collator("en", { usage: "sort" });

// Optimized for searching
const searchCollator = new Intl.Collator("en", { usage: "search" });

排序时,默认使用字符变体敏感,每个差异都会影响顺序。搜索时则优化为更宽松的敏感度以便更容易匹配结果。

要实现不区分大小写和重音符的模糊搜索:

const collator = new Intl.Collator("en", {
  usage: "search",
  sensitivity: "base"
});

const items = ["Apple", "Äpfel", "Banana"];
const matches = items.filter(item =>
  collator.compare(item, "apple") === 0
);
console.log(matches); // ["Apple"]

这种模式让用户在搜索时不需要输入完全匹配的字符,实现模糊匹配。

启用数字排序以实现自然排序

numeric 选项会将字符串中的数字作为数值处理:

const collator = new Intl.Collator("en", { numeric: true });

const files = ["File1", "File10", "File2"];
console.log(files.sort(collator.compare));
// ["File1", "File2", "File10"]

如果不启用数字排序,"File10" 会被排在 "File2" 前面,因为字符串 "10" 的首字母是 "1"。数字排序则会解析数字序列并用数学方式比较。

这样可以让文件名、版本号、编号列表等排序符合人的直观习惯。

使用数字排序处理小数

数字排序在处理小数时存在一个限制:

const collator = new Intl.Collator("en", { numeric: true });

const values = ["1.5", "1.10", "1.2"];
console.log(values.sort(collator.compare));
// ["1.2", "1.5", "1.10"]

小数点会被当作标点符号处理,不作为数字的一部分。每个标点之间的片段会被单独排序。若需对小数进行排序,请先将其解析为数字并用数值比较的方式排序。

通过 caseFirst 控制字母顺序

caseFirst 选项可决定排序时大写字母或小写字母优先:

// Uppercase first
const upperFirst = new Intl.Collator("en", { caseFirst: "upper" });
console.log(["a", "A", "b", "B"].sort(upperFirst.compare));
// ["A", "a", "B", "b"]

// Lowercase first
const lowerFirst = new Intl.Collator("en", { caseFirst: "lower" });
console.log(["a", "A", "b", "B"].sort(lowerFirst.compare));
// ["a", "A", "b", "B"]

默认值为 false,按语言环境的默认顺序排序。当 sensitivity 设置为 base 或 accent 时,该选项无效,因为这两种级别会忽略大小写。

忽略比较时的标点符号

ignorePunctuation 选项可在比较时跳过标点符号:

const collator = new Intl.Collator("en", { ignorePunctuation: true });

console.log(collator.compare("hello", "he-llo")); // 0
console.log(collator.compare("hello", "hello!")); // 0

对于泰语,该选项默认值为 true,其他语言则为 false。当你希望标点符号不影响字符串排序或匹配时,可启用此选项。

针对特定语言指定排序类型

部分语言环境支持多种排序规则以实现专业排序:

// Chinese pinyin ordering
const pinyin = new Intl.Collator("zh-CN-u-co-pinyin");

// German phonebook ordering
const phonebook = new Intl.Collator("de-DE-u-co-phonebk");

// Emoji grouping
const emoji = new Intl.Collator("en-u-co-emoji");

排序类型可在语言环境字符串中使用 Unicode 扩展语法指定。常见类型包括:

  • pinyin:按中文拼音排序
  • stroke:按中文笔画数排序
  • phonebk:德语电话簿顺序
  • trad:某些语言的传统排序规则
  • emoji:按类别对表情符号分组

请使用 Intl.supportedValuesOf 检查你环境中支持的排序类型。

在应用中复用 Collator 实例

建议创建一次 Collator 实例,在应用各处重复使用:

// utils/collation.js
export const germanCollator = new Intl.Collator("de");
export const searchCollator = new Intl.Collator("en", {
  sensitivity: "base"
});
export const numericCollator = new Intl.Collator("en", {
  numeric: true
});

// In your components
import { germanCollator } from "./utils/collation";

const sorted = names.sort(germanCollator.compare);

这种模式能最大限度提升性能,并在整个代码中保持一致的比较行为。

根据对象属性对数组排序

在比较函数中利用 collator 读取对象属性进行排序:

const collator = new Intl.Collator("de");

const users = [
  { name: "Zoe" },
  { name: "Änder" },
  { name: "Ava" }
];

const sorted = users.sort((a, b) =>
  collator.compare(a.name, b.name)
);

这种方法适用于任何对象结构。提取要比较的字符串,并将它们传递给 collator。

Intl.Collator 与 localeCompare 性能对比

在对大型数据集排序时,Intl.Collator 能提供更好的性能:

// Slower: recreates locale settings for each comparison
const slow = items.sort((a, b) => a.localeCompare(b, "de"));

// Faster: reuses precomputed locale settings
const collator = new Intl.Collator("de");
const fast = items.sort(collator.compare);

对于小数组(少于 100 项),二者几乎没有区别。对于大型数组(成千上万项),Intl.Collator 的速度可以快 60-80%。

在基于 V8 的浏览器(如 Chrome)中有一个例外。localeCompare 针对纯 ASCII 字符串使用了查找表优化。在仅包含 ASCII 字符串的排序中,localeCompare 的性能可能与 Intl.Collator 相当。

何时使用 Intl.Collator 或 localeCompare

推荐使用 Intl.Collator 的情况:

  • 排序大型数组(数百或数千项)
  • 多次排序(如用户切换排序、虚拟列表)
  • 构建可复用的比较工具
  • 性能对你的场景很重要

推荐使用 localeCompare 的情况:

  • 临时比较
  • 排序小型数组(少于 100 项)
  • 代码简单比性能更重要
  • 需要直接在代码中进行比较,无需额外设置

这两个 API 支持相同的选项,并产生相同的结果。区别仅在性能和代码结构方面。

检查实际使用的选项

resolvedOptions 方法会返回 collator 实际生效的选项:

const collator = new Intl.Collator("de", { sensitivity: "base" });
console.log(collator.resolvedOptions());
// {
//   locale: "de",
//   usage: "sort",
//   sensitivity: "base",
//   ignorePunctuation: false,
//   collation: "default",
//   numeric: false,
//   caseFirst: "false"
// }

这有助于调试排序行为并了解默认值。如果系统不支持完全一致的 locale,实际匹配到的 locale 可能会有所不同。

验证 locale 是否受支持

检查当前环境支持哪些 locale:

const supported = Intl.Collator.supportedLocalesOf(["de", "fr", "xx"]);
console.log(supported); // ["de", "fr"]

不受支持的 locale 会回退到系统默认值。此方法可以帮助检测请求的 locale 是否可用。

浏览器与环境支持

Intl.Collator 从 2017 年 9 月开始被广泛支持。所有现代浏览器和 Node.js 版本都已兼容,API 在各种环境中表现一致。

某些排序类型和选项在旧版浏览器中的支持可能有限。如果需要兼容旧环境,请务必测试关键功能或查阅 MDN 兼容性表。

常见错误须避免

不要为每次比较都新建一个 Collator 实例:

// Wrong: creates collator repeatedly
items.sort((a, b) => new Intl.Collator("de").compare(a, b));

// Right: create once, reuse
const collator = new Intl.Collator("de");
items.sort(collator.compare);

不要认为默认排序可以适用于多语言文本:

// Wrong: breaks for non-ASCII characters
names.sort();

// Right: use locale-aware sorting
names.sort(new Intl.Collator("de").compare);

进行搜索时不要忘记指定敏感度:

// Wrong: variant sensitivity requires exact match
const collator = new Intl.Collator("en");
items.filter(item => collator.compare(item, "apple") === 0);

// Right: base sensitivity for fuzzy matching
const collator = new Intl.Collator("en", { sensitivity: "base" });
items.filter(item => collator.compare(item, "apple") === 0);

实际应用场景

可在以下场景中使用 Intl.Collator:

  • 排序用户生成内容(如姓名、标题、地址)
  • 实现搜索和自动补全功能
  • 构建带可排序列的数据表
  • 创建筛选列表和下拉选项
  • 排序文件名和版本号
  • 在联系人列表中实现字母导航
  • 多语言应用界面

任何向用户展示已排序文本的界面都可以从本地化排序中受益。这样能确保你的应用无论在哪种语言环境下都显得专业且准确。