如何在 JavaScript 中按 locale 对字符串进行字母顺序排序

使用 Intl.Collator 和 localeCompare(),实现任意语言的正确字符串排序

简介

在 JavaScript 中对字符串数组进行排序时,默认行为是根据字符串的 UTF-16 码元值进行比较。这种方式适用于基础 ASCII 文本,但在排序姓名、产品标题或包含重音字符、非拉丁字符或大小写混合的文本时会出现问题。

不同语言有不同的字母顺序规则。例如,瑞典语将 å、ä 和 ö 排在字母表 z 之后。德语在大多数情况下将 ä 视为 a。法语在某些比较模式下会忽略重音符号。这些语言规则决定了用户期望在本地语言中看到的排序方式。

JavaScript 提供了两种支持 locale 感知的字符串排序 API。String.prototype.localeCompare() 方法适用于简单比较。Intl.Collator API 在排序大型数组时性能更优。本教程将介绍这两种方法的工作原理、适用场景,以及如何为不同语言配置排序行为。

为什么默认排序无法处理国际化文本

默认的 Array.sort() 方法是根据字符串的 UTF-16 码元值进行比较。这意味着大写字母总是排在小写字母前面,带重音的字符会排在 z 之后。

const names = ['Åsa', 'Anna', 'Örjan', 'Bengt', 'Ärla'];
const sorted = names.sort();
console.log(sorted);
// Output: ['Anna', 'Bengt', 'Åsa', 'Ärla', 'Örjan']

这种输出对于瑞典语来说是错误的。在瑞典语中,å、ä 和 ö 是独立的字母,应该排在字母表的末尾。正确顺序应为 Anna、Bengt,然后是 Åsa、Ärla 和 Örjan。

问题的根源在于默认排序比较的是码点值,而不是语言学意义。字母 Å 的码点是 U+00C5,比 z(U+007A)的码点大。JavaScript 并不知道瑞典语用户认为 Å 是一个独立字母,并且在字母表中有特定的位置。

大小写混合会带来另一个问题。

const words = ['zebra', 'Apple', 'banana', 'Zoo'];
const sorted = words.sort();
console.log(sorted);
// Output: ['Apple', 'Zoo', 'banana', 'zebra']

所有大写字母的编码值都比小写字母低。这会导致 Apple 和 Zoo 排在 banana 前面,这在任何语言中都不是字母顺序。

localeCompare 如何根据语言规则排序字符串

localeCompare() 方法会根据特定 locale 的排序规则比较两个字符串。如果第一个字符串排在第二个之前,则返回负数;如果两者相等,则返回零;如果第一个字符串排在第二个之后,则返回正数。

const result = 'a'.localeCompare('b', 'en-US');
console.log(result);
// Output: -1 (negative means 'a' comes before 'b')

你可以将 localeCompare() 直接作为比较函数传递给 Array.sort() 使用。

const names = ['Åsa', 'Anna', 'Örjan', 'Bengt', 'Ärla'];
const sorted = names.sort((a, b) => a.localeCompare(b, 'sv-SE'));
console.log(sorted);
// Output: ['Anna', 'Bengt', 'Åsa', 'Ärla', 'Örjan']

瑞典语 locale 会将 Anna 和 Bengt 排在前面,因为它们使用标准拉丁字母。然后是带有瑞典特殊字母的 Åsa、Ärla 和 Örjan 排在末尾。

用德语 locale 排序同样的列表会得到不同的结果。

const names = ['Åsa', 'Anna', 'Örjan', 'Bengt', 'Ärla'];
const sorted = names.sort((a, b) => a.localeCompare(b, 'de-DE'));
console.log(sorted);
// Output: ['Anna', 'Ärla', 'Åsa', 'Bengt', 'Örjan']

德语在排序时将 ä 视为与 a 等价。这会让 Ärla 紧跟在 Anna 之后,而不是像瑞典语那样排在末尾。

何时使用 localeCompare

当你需要对小数组排序或比较两个字符串时,建议使用 localeCompare()。它提供了简单的 API,无需创建和管理 collator 对象。

const items = ['Banana', 'apple', 'Cherry'];
const sorted = items.sort((a, b) => a.localeCompare(b, 'en-US'));
console.log(sorted);
// Output: ['apple', 'Banana', 'Cherry']

这种方法适用于只有几十项的数组。对于小型数据集,性能影响可以忽略不计。

你也可以用 localeCompare() 检查一个字符串是否排在另一个字符串前面,而无需对整个数组排序。

const firstName = 'Åsa';
const secondName = 'Anna';

if (firstName.localeCompare(secondName, 'sv-SE') < 0) {
  console.log(`${firstName} comes before ${secondName}`);
} else {
  console.log(`${secondName} comes before ${firstName}`);
}
// Output: "Anna comes before Åsa"

此比较遵循瑞典语字母顺序,无需对整个数组进行排序。

Intl.Collator 如何提升性能

Intl.Collator API 会创建一个可复用的比较函数,针对重复使用进行了优化。当你对大型数组排序或进行大量比较时,collator 的速度远快于每次比较都调用 localeCompare()

const collator = new Intl.Collator('sv-SE');
const names = ['Åsa', 'Anna', 'Örjan', 'Bengt', 'Ärla'];
const sorted = names.sort(collator.compare);
console.log(sorted);
// Output: ['Anna', 'Bengt', 'Åsa', 'Ärla', 'Örjan']

collator.compare 属性会返回一个可直接与 Array.sort() 配合使用的比较函数,无需用箭头函数包裹。

只需创建一次 collator 并在多次操作中复用,可避免每次比较都查找 locale 数据的开销。

const collator = new Intl.Collator('de-DE');

const germanCities = ['München', 'Berlin', 'Köln', 'Hamburg'];
const sortedCities = germanCities.sort(collator.compare);

const germanNames = ['Müller', 'Schmidt', 'Schröder', 'Fischer'];
const sortedNames = germanNames.sort(collator.compare);

console.log(sortedCities);
// Output: ['Berlin', 'Hamburg', 'Köln', 'München']

console.log(sortedNames);
// Output: ['Fischer', 'Müller', 'Schmidt', 'Schröder']

同一个 collator 可以处理多个数组,无需重复创建实例。

何时使用 Intl.Collator

当需要对包含数百或数千项的数组进行排序时,建议使用 Intl.Collator。随着数组规模增大,性能优势越明显,因为排序过程中比较函数会被频繁调用。

const collator = new Intl.Collator('en-US');
const products = [/* array with 10,000 product names */];
const sorted = products.sort(collator.compare);

对于大于几百项的数组,collator 的速度可能是 localeCompare() 的数倍。

当你需要用相同 locale 和选项对多个数组排序时,也建议使用 Intl.Collator。只需创建一次 collator 并复用,可避免重复查找 locale 数据。

const collator = new Intl.Collator('fr-FR');

const firstNames = ['Amélie', 'Bernard', 'Émilie', 'François'];
const lastNames = ['Dubois', 'Martin', 'Lefèvre', 'Bernard'];

const sortedFirstNames = firstNames.sort(collator.compare);
const sortedLastNames = lastNames.sort(collator.compare);

这种模式非常适合构建表格视图或其他需要显示多个已排序列表的界面。

如何指定 locale

localeCompare()Intl.Collator 都接受 locale 标识符作为第一个参数。该标识符采用 BCP 47 格式,通常由语言代码和可选的地区代码组成。

const names = ['Åsa', 'Anna', 'Ärla'];

// Swedish locale
const swedishSorted = names.sort((a, b) => a.localeCompare(b, 'sv-SE'));
console.log(swedishSorted);
// Output: ['Anna', 'Åsa', 'Ärla']

// German locale
const germanSorted = names.sort((a, b) => a.localeCompare(b, 'de-DE'));
console.log(germanSorted);
// Output: ['Anna', 'Ärla', 'Åsa']

区域设置决定了适用的排序规则。瑞典语和德语对 å 和 ä 有不同的规则,这会导致不同的排序顺序。

你可以省略 locale,以使用用户浏览器的默认区域设置。

const collator = new Intl.Collator();
const names = ['Åsa', 'Anna', 'Ärla'];
const sorted = names.sort(collator.compare);

这种方法尊重用户的语言偏好,无需硬编码特定的区域设置。排序结果将根据用户的浏览器设置,符合其预期。

你还可以传递一个 locales 数组来提供备用选项。

const collator = new Intl.Collator(['sv-SE', 'sv', 'en-US']);

API 会使用数组中第一个受支持的区域设置。如果瑞典(瑞典)不可用,则尝试通用瑞典语,最后回退到美国英语。

如何控制大小写敏感性

sensitivity 选项用于控制比较时如何处理大小写和重音符号的差异。它接受四个值:baseaccentcasevariant

base 敏感性会忽略大小写和重音符号,仅比较基础字符。

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

console.log(collator.compare('a', 'A'));
// Output: 0 (equal)

console.log(collator.compare('a', 'á'));
// Output: 0 (equal)

console.log(collator.compare('a', 'b'));
// Output: -1 (different base characters)

此模式下,a、A 和 á 被视为相同,因为它们具有相同的基础字符。

accent 敏感性会考虑重音符号,但忽略大小写。

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

console.log(collator.compare('a', 'A'));
// Output: 0 (equal, case ignored)

console.log(collator.compare('a', 'á'));
// Output: -1 (different, accent matters)

case 敏感性会考虑大小写,但忽略重音符号。

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

console.log(collator.compare('a', 'A'));
// Output: -1 (different, case matters)

console.log(collator.compare('a', 'á'));
// Output: 0 (equal, accent ignored)

variant 敏感性(默认)会考虑所有差异。

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

console.log(collator.compare('a', 'A'));
// Output: -1 (different)

console.log(collator.compare('a', 'á'));
// Output: -1 (different)

此模式提供最严格的比较,任何差异都会被视为重要。

如何对包含数字的字符串进行排序

numeric 选项可为包含数字的字符串启用数字排序。启用后,比较会将数字序列作为数值处理,而不是逐字符比较。

const files = ['file1.txt', 'file10.txt', 'file2.txt', 'file20.txt'];

// Default sorting (wrong order)
const defaultSorted = [...files].sort();
console.log(defaultSorted);
// Output: ['file1.txt', 'file10.txt', 'file2.txt', 'file20.txt']

// Numeric sorting (correct order)
const collator = new Intl.Collator('en-US', { numeric: true });
const numericSorted = files.sort(collator.compare);
console.log(numericSorted);
// Output: ['file1.txt', 'file2.txt', 'file10.txt', 'file20.txt']

如果不进行数字排序,字符串会按字符逐个比较。字符串 10 会排在 2 前面,因为第一个字符 1 的码点比 2 更小。

启用数字排序后,排序器会将 10 识别为数字 10,而 2 识别为数字 2。这样会得到预期的排序结果,即 2 排在 10 前面。

此选项适用于对文件名、版本号或包含文本和数字的字符串进行排序。

const versions = ['v1.10', 'v1.2', 'v1.20', 'v1.3'];
const collator = new Intl.Collator('en-US', { numeric: true });
const sorted = versions.sort(collator.compare);
console.log(sorted);
// Output: ['v1.2', 'v1.3', 'v1.10', 'v1.20']

如何控制大小写排序优先级

caseFirst 选项用于指定在仅大小写不同的字符串比较时,先排序大写还是小写。它接受三个值:upperlowerfalse

const words = ['apple', 'Apple', 'APPLE'];

// Uppercase first
const upperFirst = new Intl.Collator('en-US', { caseFirst: 'upper' });
const upperSorted = [...words].sort(upperFirst.compare);
console.log(upperSorted);
// Output: ['APPLE', 'Apple', 'apple']

// Lowercase first
const lowerFirst = new Intl.Collator('en-US', { caseFirst: 'lower' });
const lowerSorted = [...words].sort(lowerFirst.compare);
console.log(lowerSorted);
// Output: ['apple', 'Apple', 'APPLE']

// Default (locale-dependent)
const defaultCase = new Intl.Collator('en-US', { caseFirst: 'false' });
const defaultSorted = [...words].sort(defaultCase.compare);
console.log(defaultSorted);
// Output depends on locale

false 表示使用本地化默认的大小写排序顺序。大多数本地化环境在默认敏感度设置下,会将仅大小写不同的字符串视为相等。

只有当 sensitivity 选项允许区分大小写时,此选项才会生效。

如何在排序时忽略标点符号

ignorePunctuation 选项会让排序器在比较字符串时跳过标点符号。当需要对可能包含或不包含标点的标题或短语进行排序时,这个选项非常有用。

const titles = [
  'The Old Man',
  'The Old-Man',
  'The Oldman',
];

// Default (punctuation matters)
const defaultCollator = new Intl.Collator('en-US');
const defaultSorted = [...titles].sort(defaultCollator.compare);
console.log(defaultSorted);
// Output: ['The Old Man', 'The Old-Man', 'The Oldman']

// Ignore punctuation
const noPunctCollator = new Intl.Collator('en-US', { ignorePunctuation: true });
const noPunctSorted = [...titles].sort(noPunctCollator.compare);
console.log(noPunctSorted);
// Output: ['The Old Man', 'The Old-Man', 'The Oldman']

当忽略标点符号时,比较会将 "Old-Man" 中的连字符视为不存在,使这些字符串的比较结果就像都是 "TheOldMan" 一样。

对来自不同国家用户的用户名进行排序

在对全球用户的姓名进行排序时,应使用用户的首选本地化设置,以尊重其语言习惯。

const userLocale = navigator.language;
const collator = new Intl.Collator(userLocale);

const users = [
  { name: 'Müller', country: 'Germany' },
  { name: 'Martin', country: 'France' },
  { name: 'Andersson', country: 'Sweden' },
  { name: 'García', country: 'Spain' },
];

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

sorted.forEach(user => {
  console.log(`${user.name} (${user.country})`);
});

此代码会根据浏览器检测用户的本地化设置,并相应地对姓名进行排序。德国用户会看到按德语规则排序的列表,瑞典用户则会看到按瑞典语规则排序的列表。

支持切换本地化的排序

当应用允许用户切换语言时,应在本地化设置变更时更新 collator。

let currentLocale = 'en-US';
let collator = new Intl.Collator(currentLocale);

function setLocale(newLocale) {
  currentLocale = newLocale;
  collator = new Intl.Collator(currentLocale);
}

function sortItems(items) {
  return items.sort(collator.compare);
}

// User switches to Swedish
setLocale('sv-SE');
const names = ['Åsa', 'Anna', 'Örjan'];
console.log(sortItems(names));
// Output: ['Anna', 'Åsa', 'Örjan']

// User switches to German
setLocale('de-DE');
const germanNames = ['Über', 'Uhr', 'Udo'];
console.log(sortItems(germanNames));
// Output: ['Udo', 'Uhr', 'Über']

这种模式可确保排序列表会根据用户选择的语言实时更新。

如何选择 localeCompare 与 Intl.Collator

当你只需快速比较一次或对少于 100 项的小数组排序时,建议使用 localeCompare()。其语法更简单,且对于小型数据集性能差异可以忽略。

const items = ['banana', 'Apple', 'cherry'];
const sorted = items.sort((a, b) => a.localeCompare(b, 'en-US'));

当需要对大型数组排序、执行大量比较,或用相同本地化和选项对多个数组排序时,建议使用 Intl.Collator。只需创建一次 collator 并复用,可获得更优性能。

const collator = new Intl.Collator('en-US', { sensitivity: 'base', numeric: true });

const products = [/* large array */];
const sorted = products.sort(collator.compare);

两种方式都能得到相同的结果,选择取决于你的性能需求和代码组织偏好。