如何比较字符串以忽略大小写差异

使用支持区域设置的比较方法,正确处理跨语言的不区分大小写匹配

简介

大小写不敏感的字符串比较在 Web 应用程序中经常出现。用户在输入搜索查询时可能会混合大小写,输入用户名时可能会使用不一致的大小写,或者在填写表单时不考虑字母的大小写。您的应用程序需要正确匹配这些输入,无论用户输入的是大写、小写还是混合大小写。

一种直接的方法是将两个字符串都转换为小写后再进行比较。这种方法适用于英文文本,但对于国际化应用程序则会失败。不同语言在大写和小写之间的转换规则各不相同。一种适用于英文的比较方法可能会对土耳其语、德语、希腊语或其他语言产生错误的结果。

JavaScript 提供了 Intl.Collator API,可以在所有语言中正确处理大小写不敏感的比较。本课程将解释为什么简单的小写转换会失败,语言环境感知的比较是如何工作的,以及何时使用每种方法。

使用 toLowerCase 的简单方法

在比较之前将两个字符串都转换为小写是实现大小写不敏感匹配的最常见方法:

const str1 = "Hello";
const str2 = "HELLO";

console.log(str1.toLowerCase() === str2.toLowerCase());
// true

这种模式适用于 ASCII 文本和英文单词。比较将同一字母的大写和小写版本视为相同。

您可以将此方法用于模糊搜索:

const query = "apple";
const items = ["Apple", "Banana", "APPLE PIE", "Orange"];

const matches = items.filter(item =>
  item.toLowerCase().includes(query.toLowerCase())
);

console.log(matches);
// ["Apple", "APPLE PIE"]

过滤器会找到所有包含搜索查询的项目,而不考虑大小写。这为用户提供了预期的行为,即使他们在输入查询时没有考虑大小写。

为什么简单方法在国际文本中会失败

toLowerCase() 方法根据 Unicode 规则转换文本,但这些规则在所有语言中并不完全相同。最著名的例子是土耳其语中的 i 问题。

在英语中,小写字母 i 转换为大写字母 I。而在土耳其语中,有两个不同的字母:

  • 小写带点的 i 转换为大写带点的 İ
  • 小写不带点的 ı 转换为大写不带点的 I

这种差异会破坏不区分大小写的比较:

const word1 = "file";
const word2 = "FILE";

// 在英语区域设置中(正确)
console.log(word1.toLowerCase() === word2.toLowerCase());
// true

// 在土耳其语区域设置中(错误)
console.log(word1.toLocaleLowerCase("tr") === word2.toLocaleLowerCase("tr"));
// false - "file" 变成了 "fıle"

当使用土耳其语规则将 FILE 转换为小写时,I 变成了 ı(不带点),生成了 fıle。这与 file(带点的 i)不匹配,因此即使字符串表示相同的单词,比较结果仍然是 false。

其他语言也有类似的问题。德语中有 ß 字符,其大写形式为 SS。希腊语中有多个小写形式的西格玛(σς),它们的大写形式都是 Σ。简单的大小写转换无法正确处理这些特定语言的规则。

使用 Intl.Collator 的基础敏感度进行不区分大小写的比较

Intl.Collator API 提供了支持区域设置的字符串比较,并具有可配置的敏感度选项。sensitivity 选项控制比较时哪些差异是重要的。

对于不区分大小写的比较,可以使用 sensitivity: "base"

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

console.log(collator.compare("Hello", "hello"));
// 0(字符串相等)

console.log(collator.compare("Hello", "HELLO"));
// 0(字符串相等)

console.log(collator.compare("Hello", "Héllo"));
// 0(字符串相等,重音符号也被忽略)

基础敏感度忽略大小写和重音符号的差异。只有基础字母才重要。当字符串在此敏感度级别上等价时,比较结果为 0。

这种方法可以正确处理土耳其语的 i 问题:

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

console.log(collator.compare("file", "FILE"));
// 0(正确匹配)

console.log(collator.compare("file", "FİLE"));
// 0(正确匹配,即使带有带点的 İ)

比较器会自动应用土耳其语的大小写折叠规则。两次比较都将字符串识别为等价的,无论输入中出现的是哪种大写 I。

使用 localeCompare 和 sensitivity 选项

localeCompare() 方法提供了一种执行不区分大小写比较的替代方式。它接受与 Intl.Collator 相同的选项:

const str1 = "Hello";
const str2 = "HELLO";

console.log(str1.localeCompare(str2, "en", { sensitivity: "base" }));
// 0(字符串相等)

这与使用 Intl.Collator 的 base sensitivity 选项产生相同的结果。比较会忽略大小写差异,并对等价字符串返回 0。

您可以在数组过滤中使用此方法:

const query = "apple";
const items = ["Apple", "Banana", "APPLE PIE", "Orange"];

const matches = items.filter(item =>
  item.localeCompare(query, "en", { sensitivity: "base" }) === 0 ||
  item.toLowerCase().includes(query.toLowerCase())
);

console.log(matches);
// ["Apple"]

然而,localeCompare() 仅在指定的敏感度级别下对完全匹配返回 0。它不支持像 includes() 那样的部分匹配。对于子字符串搜索,您仍然需要使用小写转换或实现更复杂的搜索算法。

在 base 和 accent sensitivity 之间选择

sensitivity 选项接受四个值,用于控制字符串比较的不同方面:

Base sensitivity

Base sensitivity 会忽略大小写和重音符号:

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

console.log(collator.compare("cafe", "café"));
// 0(忽略重音符号)

console.log(collator.compare("cafe", "Café"));
// 0(忽略大小写和重音符号)

console.log(collator.compare("cafe", "CAFÉ"));
// 0(忽略大小写和重音符号)

这提供了最宽松的匹配方式。即使用户无法输入重音字符或为了方便省略它们,也能获得正确的匹配结果。

Accent sensitivity

Accent sensitivity 会忽略大小写,但会考虑重音符号:

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

console.log(collator.compare("cafe", "café"));
// -1(重音符号有区别)

console.log(collator.compare("cafe", "Café"));
// -1(重音符号有区别,忽略大小写)

console.log(collator.compare("Café", "CAFÉ"));
// 0(忽略大小写,重音符号匹配)

这种方式将带重音和不带重音的字母视为不同,但忽略大小写。当重音符号的差异很重要而大小写差异不重要时,可以使用此选项。

为您的用例选择合适的敏感度

对于大多数不区分大小写的比较需求,基础敏感度提供了最佳的用户体验:

  • 用户在搜索功能中输入不带重音符号的查询
  • 用户名匹配时不应区分大小写
  • 模糊查找时需要最大灵活性
  • 表单验证中 Smithsmith 应该匹配

在以下情况下使用重音敏感度:

  • 语言需要区分带重音符号的字符
  • 数据中同时包含带重音和不带重音的版本且含义不同
  • 您需要不区分大小写但区分重音的比较

使用 includes 执行不区分大小写的搜索

Intl.Collator API 比较完整的字符串,但不提供子字符串匹配功能。对于不区分大小写的搜索,您仍需将基于区域设置的比较与其他方法结合使用。

一种方法是使用 toLowerCase() 进行子字符串搜索,但需接受其在国际文本中的局限性:

function caseInsensitiveIncludes(text, query, locale = "en") {
  return text.toLowerCase().includes(query.toLowerCase());
}

const text = "The Quick Brown Fox";
console.log(caseInsensitiveIncludes(text, "quick"));
// true

对于更复杂的搜索需求,正确处理国际文本,您需要遍历可能的子字符串位置,并为每次比较使用 collator:

function localeAwareIncludes(text, query, locale = "en") {
  const collator = new Intl.Collator(locale, { sensitivity: "base" });

  for (let i = 0; i <= text.length - query.length; i++) {
    const substring = text.slice(i, i + query.length);
    if (collator.compare(substring, query) === 0) {
      return true;
    }
  }

  return false;
}

const text = "The Quick Brown Fox";
console.log(localeAwareIncludes(text, "quick"));
// true

此方法检查每个可能的正确长度的子字符串,并为每次比较使用基于区域设置的比较。它可以正确处理国际文本,但性能比简单的 includes() 差。

使用 Intl.Collator 时的性能注意事项

创建一个 Intl.Collator 实例需要加载区域设置数据并处理选项。当需要执行多次比较时,应一次性创建 collator 并重复使用它:

// 效率低:每次比较都创建 collator
function badCompare(items, target) {
  return items.filter(item =>
    new Intl.Collator("en", { sensitivity: "base" }).compare(item, target) === 0
  );
}

// 高效:一次性创建 collator 并重复使用
function goodCompare(items, target) {
  const collator = new Intl.Collator("en", { sensitivity: "base" });
  return items.filter(item =>
    collator.compare(item, target) === 0
  );
}

高效版本在过滤之前一次性创建 collator。每次比较都使用同一个实例,避免了重复初始化的开销。

对于需要频繁比较的应用程序,可以在应用程序启动时创建 collator 实例,并将其导出以供代码库中的其他部分使用:

// utils/collation.js
export const caseInsensitiveCollator = new Intl.Collator("en", {
  sensitivity: "base"
});

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

// 在应用程序代码中
import { caseInsensitiveCollator } from "./utils/collation";

const isMatch = caseInsensitiveCollator.compare(input, expected) === 0;

这种模式最大化了性能,并在整个应用程序中保持一致的比较行为。

何时使用 toLowerCase 与 Intl.Collator

对于仅支持英语的应用程序,如果您可以控制文本内容并且知道它仅包含 ASCII 字符,toLowerCase() 可以提供可接受的结果:

// 对于仅支持英语和 ASCII 的文本是可接受的
const isMatch = str1.toLowerCase() === str2.toLowerCase();

这种方法简单、快速,并且大多数开发人员都熟悉。如果您的应用程序确实从未处理国际化文本,那么使用区域设置感知的比较可能不会带来额外的价值。

对于国际化应用程序或用户可以输入任何语言文本的应用程序,请使用带有适当敏感度的 Intl.Collator

// 适用于国际化文本
const collator = new Intl.Collator(userLocale, { sensitivity: "base" });
const isMatch = collator.compare(str1, str2) === 0;

这可以确保无论用户使用哪种语言输入,都能获得正确的行为。使用 Intl.Collator 的小性能成本是值得的,可以避免错误的比较。

即使您的应用程序当前仅支持英语,从一开始就使用区域设置感知的比较可以使未来的国际化更容易。添加对新语言的支持时,无需更改比较逻辑。

不区分大小写比较的实际用例

不区分大小写的比较出现在许多常见场景中:

用户名和电子邮件匹配

用户输入用户名和电子邮件地址时,可能会使用不一致的大小写:

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

function findUserByEmail(users, email) {
  return users.find(user =>
    collator.compare(user.email, email) === 0
  );
}

const users = [
  { email: "[email protected]", name: "John" },
  { email: "[email protected]", name: "Jane" }
];

console.log(findUserByEmail(users, "[email protected]"));
// { email: "[email protected]", name: "John" }

这可以找到用户,无论他们如何输入电子邮件地址的大小写。

搜索自动完成

自动完成建议需要与部分输入进行不区分大小写的匹配:

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

function getSuggestions(items, query) {
  const queryLower = query.toLowerCase();

  return items.filter(item =>
    item.toLowerCase().startsWith(queryLower)
  );
}

const items = ["Apple", "Apricot", "Banana", "Cherry"];
console.log(getSuggestions(items, "ap"));
// ["Apple", "Apricot"]

这可以提供建议,无论用户输入的大小写如何。

标签和类别匹配

用户为内容分配标签或类别时,可能会使用不一致的大小写:

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

function hasTag(item, tag) {
  return item.tags.some(itemTag =>
    collator.compare(itemTag, tag) === 0
  );
}

const article = {
  title: "My Article",
  tags: ["JavaScript", "Tutorial", "Web Development"]
};

console.log(hasTag(article, "javascript"));
// true

这可以匹配标签,无论大小写差异如何。