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

使用支持本地化的比较方法,确保在不同语言中正确处理不区分大小写的匹配

引言

在 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";

// In English locale (correct)
console.log(word1.toLowerCase() === word2.toLowerCase());
// true

// In Turkish locale (incorrect)
console.log(word1.toLocaleLowerCase("tr") === word2.toLocaleLowerCase("tr"));
// false - "file" becomes "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 (strings are equal)

console.log(collator.compare("Hello", "HELLO"));
// 0 (strings are equal)

console.log(collator.compare("Hello", "Héllo"));
// 0 (strings are equal, accents ignored too)

基础敏感度会忽略大小写和重音符号的差异,仅比较基本字母。当字符串在此敏感度下等价时,比较结果为 0。

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

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

console.log(collator.compare("file", "FILE"));
// 0 (correctly matches)

console.log(collator.compare("file", "FİLE"));
// 0 (correctly matches, even with dotted İ)

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

使用 localeCompare 的 sensitivity 选项

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

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

console.log(str1.localeCompare(str2, "en", { sensitivity: "base" }));
// 0 (strings are equal)

这将产生与使用 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() 只会在指定 sensitivity 级别下完全匹配时返回 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 (accents ignored)

console.log(collator.compare("cafe", "Café"));
// 0 (case and accents ignored)

console.log(collator.compare("cafe", "CAFÉ"));
// 0 (case and accents ignored)

这提供了最宽松的匹配方式。无法输入重音字符或为了方便省略重音的用户,依然可以获得正确的匹配结果。

Accent sensitivity

Accent sensitivity 忽略大小写,但区分重音符号:

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

console.log(collator.compare("cafe", "café"));
// -1 (accents matter)

console.log(collator.compare("cafe", "Café"));
// -1 (accents matter, case ignored)

console.log(collator.compare("Café", "CAFÉ"));
// 0 (case ignored, accents match)

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

针对不同场景选择合适的 sensitivity

对于大多数不区分大小写的比较需求,base sensitivity 能带来最佳用户体验:

  • 搜索功能,用户输入时不带重音符号
  • 用户名匹配,不区分大小写
  • 模糊查找,需最大灵活性
  • 表单校验,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 并复用:

// Inefficient: creates collator for every comparison
function badCompare(items, target) {
  return items.filter(item =>
    new Intl.Collator("en", { sensitivity: "base" }).compare(item, target) === 0
  );
}

// Efficient: creates collator once, reuses it
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"
});

// In your application code
import { caseInsensitiveCollator } from "./utils/collation";

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

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

何时使用 toLowerCase 与 Intl.Collator

对于仅支持英文且文本内容可控、只包含 ASCII 字符的应用,toLowerCase() 可以获得可接受的结果:

// Acceptable for English-only, ASCII-only text
const isMatch = str1.toLowerCase() === str2.toLowerCase();

这种方法简单、快速,并且大多数开发者都很熟悉。如果你的应用确实从不处理国际化文本,那么引入支持本地化的比较所带来的复杂性可能并没有实际价值。

对于国际化应用,或者用户可能输入任意语言文本的应用,应使用 Intl.Collator 并设置合适的 sensitivity 参数:

// Required for international text
const collator = new Intl.Collator(userLocale, { sensitivity: "base" });
const isMatch = collator.compare(str1, str2) === 0;

这样可以确保无论用户使用哪种语言输入或书写,都能获得正确的行为。使用 Intl.Collator 带来的轻微性能开销是值得的,可以避免错误的比较结果。

即使你的应用目前只支持 English,从一开始就采用本地化比较也能让未来的国际化工作更加轻松。添加新语言支持时,无需更改比较逻辑。

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

不区分大小写的比较在许多常见场景中都会用到:

用户名和邮箱匹配

用户输入用户名和邮箱地址时,大小写经常不一致:

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

这样可以无论大小写差异如何,都能正确匹配标签。