如何在 JavaScript 中按语言环境对字符串进行字母顺序排序

使用 Intl.Collator 和 localeCompare() 为任何语言正确排序字符串

介绍

当你在 JavaScript 中对字符串数组进行排序时,默认行为是根据字符串的 UTF-16 代码单元值进行比较。这种方法适用于基本的 ASCII 文本,但在排序包含重音字符、非拉丁字母或大小写混合的文本(如姓名或产品标题)时会失败。

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

JavaScript 提供了两个支持区域设置的字符串排序 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);
// 输出: ['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);
// 输出: ['Apple', 'Zoo', 'banana', 'zebra']

所有大写字母的代码点值都比小写字母低。这导致 Apple 和 Zoo 排在 banana 之前,这在任何语言中都不符合字母顺序。

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

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

const result = 'a'.localeCompare('b', 'en-US');
console.log(result);
// 输出: -1 (负数表示 'a' 排在 '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);
// 输出: ['Anna', 'Bengt', 'Åsa', 'Ärla', 'Örjan']

在瑞典语环境中,Anna 和 Bengt 排在最前面,因为它们使用标准的拉丁字母。然后是 Åsa、Ärla 和 Örjan,它们的特殊瑞典字母排在最后。

使用德语环境对同一列表进行排序会产生不同的结果。

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

在德语中,ä 在排序时被视为等同于 a。这使得 Ärla 紧跟在 Anna 之后,而不是像瑞典语那样排在最后。

何时使用 localeCompare

当您需要对一个小数组进行排序或比较两个字符串时,可以使用 localeCompare()。它提供了一个简单的 API,而无需创建和管理一个比较器对象。

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

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

您还可以使用 localeCompare() 来检查一个字符串是否排在另一个字符串之前,而无需对整个数组进行排序。

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

if (firstName.localeCompare(secondName, 'sv-SE') < 0) {
  console.log(`${firstName} 排在 ${secondName} 之前`);
} else {
  console.log(`${secondName} 排在 ${firstName} 之前`);
}
// 输出: "Anna 排在 Å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);
// 输出: ['Anna', 'Bengt', 'Åsa', 'Ärla', 'Örjan']

collator.compare 属性返回一个可以直接与 Array.sort() 一起使用的比较函数。您无需将其包装在箭头函数中。

创建一个 collator 并在多个操作中重复使用,可以避免每次比较时查找区域设置数据的开销。

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);
// 输出: ['Berlin', 'Hamburg', 'Köln', 'München']

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

同一个 collator 可以处理多个数组,而无需创建新的实例。

何时使用 Intl.Collator

当对包含数百或数千个项目的数组进行排序时,请使用 Intl.Collator。随着数组大小的增加,性能优势会更加明显,因为在排序过程中比较函数会被多次调用。

const collator = new Intl.Collator('en-US');
const products = [/* 包含 10,000 个产品名称的数组 */];
const sorted = products.sort(collator.compare);

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

当您需要使用相同的区域设置和选项对多个数组进行排序时,也可以使用 Intl.Collator。创建一次 collator 并重复使用它,可以消除重复的区域设置数据查找。

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);

这种模式在构建表格视图或其他显示多个排序列表的界面时非常有效。

如何指定语言环境

localeCompare()Intl.Collator 都接受一个语言环境标识符作为它们的第一个参数。这个标识符使用 BCP 47 格式,通常由语言代码和可选的区域代码组成。

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

// 瑞典语环境
const swedishSorted = names.sort((a, b) => a.localeCompare(b, 'sv-SE'));
console.log(swedishSorted);
// 输出: ['Anna', 'Åsa', 'Ärla']

// 德语环境
const germanSorted = names.sort((a, b) => a.localeCompare(b, 'de-DE'));
console.log(germanSorted);
// 输出: ['Anna', 'Ärla', 'Åsa']

语言环境决定了使用哪种排序规则。瑞典语和德语对 å 和 ä 的排序规则不同,因此会产生不同的排序结果。

您可以省略语言环境以使用浏览器的默认语言环境。

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

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

您还可以传递一个语言环境数组以提供备用选项。

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'));
// 输出: 0(相等)

console.log(collator.compare('a', 'á'));
// 输出: 0(相等)

console.log(collator.compare('a', 'b'));
// 输出: -1(基础字符不同)

此模式将 a、A 和 á 视为相同,因为它们共享相同的基础字符。

accent 敏感性考虑重音但忽略大小写。

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

console.log(collator.compare('a', 'A'));
// 输出: 0(相等,忽略大小写)

console.log(collator.compare('a', 'á'));
// 输出: -1(不同,重音重要)

case 敏感性考虑大小写但忽略重音。

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

console.log(collator.compare('a', 'A'));
// 输出: -1(不同,大小写重要)

console.log(collator.compare('a', 'á'));
// 输出: 0(相等,忽略重音)

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

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

console.log(collator.compare('a', 'A'));
// 输出: -1(不同)

console.log(collator.compare('a', 'á'));
// 输出: -1(不同)

此模式提供最严格的比较,将任何差异视为显著差异。

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

numeric 选项启用对包含数字的字符串的数字排序。启用后,比较会将数字序列视为数值,而不是逐字符进行比较。

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

// 默认排序(错误顺序)
const defaultSorted = [...files].sort();
console.log(defaultSorted);
// 输出: ['file1.txt', 'file10.txt', 'file2.txt', 'file20.txt']

// 数字排序(正确顺序)
const collator = new Intl.Collator('en-US', { numeric: true });
const numericSorted = files.sort(collator.compare);
console.log(numericSorted);
// 输出: ['file1.txt', 'file2.txt', 'file10.txt', 'file20.txt']

在没有数字排序的情况下,字符串是逐字符排序的。字符串 10 会排在 2 之前,因为第一个字符 1 的代码点比 2 小。

启用数字排序后,比较器会将 10 识别为数字十,将 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);
// 输出: ['v1.2', 'v1.3', 'v1.10', 'v1.20']

如何控制大小写的排序顺序

caseFirst 选项决定在比较仅大小写不同的字符串时,大写字母或小写字母优先排序。它接受三个值:upperlowerfalse

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

// 大写优先
const upperFirst = new Intl.Collator('en-US', { caseFirst: 'upper' });
const upperSorted = [...words].sort(upperFirst.compare);
console.log(upperSorted);
// 输出: ['APPLE', 'Apple', 'apple']

// 小写优先
const lowerFirst = new Intl.Collator('en-US', { caseFirst: 'lower' });
const lowerSorted = [...words].sort(lowerFirst.compare);
console.log(lowerSorted);
// 输出: ['apple', 'Apple', 'APPLE']

// 默认(依赖于语言环境)
const defaultCase = new Intl.Collator('en-US', { caseFirst: 'false' });
const defaultSorted = [...words].sort(defaultCase.compare);
console.log(defaultSorted);
// 输出取决于语言环境

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

此选项仅在 sensitivity 选项允许大小写差异生效时才有效。

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

ignorePunctuation 选项指示排序器在比较字符串时跳过标点符号。这在排序可能包含或不包含标点符号的标题或短语时非常有用。

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

// 默认(标点符号重要)
const defaultCollator = new Intl.Collator('en-US');
const defaultSorted = [...titles].sort(defaultCollator.compare);
console.log(defaultSorted);
// 输出: ['The Old Man', 'The Old-Man', 'The Oldman']

// 忽略标点符号
const noPunctCollator = new Intl.Collator('en-US', { ignorePunctuation: true });
const noPunctSorted = [...titles].sort(noPunctCollator.compare);
console.log(noPunctSorted);
// 输出: ['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})`);
});

此代码从浏览器中检测用户的语言环境,并相应地对名称进行排序。德国用户会看到按德国规则排序的列表,而瑞典用户会看到按瑞典规则排序的列表。

支持语言切换的排序

当您的应用程序允许用户切换语言时,在语言环境更改时更新排序器。

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);
}

// 用户切换到瑞典语
setLocale('sv-SE');
const names = ['Åsa', 'Anna', 'Örjan'];
console.log(sortItems(names));
// 输出: ['Anna', 'Åsa', 'Örjan']

// 用户切换到德语
setLocale('de-DE');
const germanNames = ['Über', 'Uhr', 'Udo'];
console.log(sortItems(germanNames));
// 输出: ['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);

这两种方法都能产生相同的结果。选择取决于您的性能需求和代码组织偏好。