regex 总览待审核
字符的真面目 · 大小写不是查一张表

大小写:为什么 toLowerCase 不够,正则 i 走的是另一套

大小写看着是最简单的一对(aA),却藏着三个陷阱:转换会改长度(ß 的大写是两个字母 SS)、会依赖 locale(土耳其语的 I 转小写不是 i)、而且「给人看的转换」(case mapping)和「给比较用的折叠」(case folding)是两套不同的规则。正则的 i 标志走的是后者。本页把这几层拆开,并对照 归一化那页——它和大小写折叠同属「比较前先把同义写法拍平」的家族。

四种操作:lowercase / uppercase / titlecase 映射,与 case folding

Unicode 把「变换大小写」分成映射(case mapping)和折叠(case folding)两件事,各有用途:

字符toLowerCasetoUpperCase说明
ß U+00DFßSS德语 eszett,upper 变两个字母 —— 大小写会改长度
U+FB01FI连字 fi,upper 拆成两个字母
İ U+0130İ带点大写 I,默认 lower 得 i+组合点(两码点)
Σ U+03A3σ / ςΣ希腊 sigma,词尾小写是 ς、词中是 σ —— 上下文相关
Dž U+01C5džDŽ本身就是 titlecase 形,upper / title / lower 三态都不同
K U+212AkK开尔文符号,case fold 后等于普通 k(正则 i 因此能互相匹配)
同一个字符,四种操作可能给出四种结果;长度、字形、locale 都可能变。

亲手试:大小写转换会改长度、改字形

输入一个字符,看 toLowerCase / toUpperCase 各把它变成什么,以及三层长度(UTF-16 码元 / code point / 字素簇)有没有变。full case mapping 的字符(ß )转换后码点数会增加:

locale 相关:同一个 I,英文和土耳其结果不同

case mapping 不是全球统一的查表。最经典的是土耳其语 / 阿塞拜疆语:它们区分无点 i(i / I 之外另有 ı / İ)。同一个 I,英文规则转小写得 i,土耳其规则得无点的 ı —— 大小写不分语言地乱转,会把名字写错:

实务提醒:把字符串「转小写后比较」是常见的大小写无关比较手法,但不要用带 locale 的 toLocaleLowerCase() 做这件事 —— 在土耳其语环境下 "I".toLocaleLowerCase()ı,会让 "FILE""file" 比不相等。无关 locale 的规范化比较应该用 case folding(见下一节)。

正则 i 标志 = case folding,不是 toLowerCase

开了 i 的正则用 Unicode case folding 判等,这比「两边各 toLowerCase 再比」更严谨,能认出 K(开尔文 U+212A)= kſ(长 s U+017F)= s 这类。下面对每一对字符同时跑两条:/A/iu.test(B)A.toLowerCase() === B.toLowerCase(),看它们何时给出不同答案:

每对:左字符当 pattern(加 iu)、右字符当待测串

simple vs full:JS 原生正则的 i 用的是 simple case folding(1 对 1),所以它认 K=kſ=s,但ßss(那是 full folding 的 1 对多)。这也是为什么 /straße/iu.test("STRASSE") 返回 false。本仓库 自研引擎那页i 同样走折叠等价表。

大小写相关的码点属性 \p{…}

UCD 里和大小写有关的二元属性,JS 正则都能直接查。输入一个字符,看它命中哪些(\p{Cased} 标记「有大小写之分」、\p{Case_Ignorable} 是大小写无关的附属符号、Changes_When_* 标记某种转换会不会改变它):

一句话收束:给人看用 case mapping(注意 locale、注意可能改长度);给机器做大小写无关的比较 / 去重 / 查找用 case folding(正则 i 就是它)。两者都是「同义写法拍平」,和 归一化互补 —— 严谨的做法常常是先 normalize 再 fold。土耳其 i 这类还提醒:涉及具体语言的转换,务必显式带 locale。