字母转小写

整段文本按 Unicode 默认大小写映射转成全小写。土耳其语的 I、希腊语的词尾 Σ、德语的 ß 各有各的规矩,本页把码位变化逐条摊开给你看。

输出码元数74
输出码点数73
码点变化+1
发生映射的码点24
输入行数5
发生变化的行5
ASCII 大写字母数13
已是无小写,保持不变48

三条与输入无关的固定规则

  • ß(U+00DF)转小写不变,因为它本身已是小写;展开成 SS 的是转大写那一侧。含义:STRASSE 与 straße 的小写形式不相等,做去重键时要先统一这组字符。
  • emoji、汉字与增补平面数学字母(U+1D400 起)全部原样保留——它们在 Unicode 大小写表里没有映射。字体里的花体字母若用普通码位则照常转换。
  • toLowerCase 是幂等的(f(f(x)) = f(x)),对已全小的文本再转一次结果不变,「映射 0 个」不是出错。

使用方法

  1. 把要转换的文本粘进输入框。页面打开时已经预置了 5 行示例,包含土耳其语、希腊语、德语、汉字与 emoji,下面的统计卡显示的就是它的实测结果。
  2. 选语言环境。默认是 Unicode 默认映射,跨浏览器一致;选 tr 会让不带点的 I 转成土耳其语的 ı(U+0131)而不是 i。只有处理土耳其语内容时才需要切。
  3. 点「转成小写」,或在输入框里按 Ctrl + Enter(macOS 用 Cmd + Enter)立即重算。文本框里的 Enter 保留给换行。
  4. 看码点变化这一项。它是本页最关键的口径:默认模式下示例文本的码点数增加了 1,因为土耳其语的 İ(U+0130)转成 i 加一个组合尖音符(U+0307)——一个字符变成了两个码点。切到 tr 时码点数不变。
  5. 点「只看变化的行」把结果区收窄到实际发生变化的行,其余行折叠显示;点「复制结果」把全部小写文本带走;点「载入示例」回到默认 5 行;点「清空」清空(空文本是合法输入,会照常输出空结果,不报错)。

这个字母转小写工具能解决什么问题

一是把 URL、文件名、路由统一成小写。多数 Web 服务器在 Linux 上区分大小写:把 /Product/Detail 部署成 /product/detail 后,所有指向大写路径的旧链接会 404。批量转一遍再改配置是最省事的做法——但要注意这不是 slugify:本页只改大小写,不会把空格换成连字符、不会去掉标点。要那套处理用「URL别名生成器」或「Slugify字符串工具」。

二是做大小写不敏感的去重与比对。两个文件名 README.md 和 readme.md 在 Windows 上是同一个文件,在 Git 仓库里却是两条记录。统一转小写后去重,能扫出这类重复提交。注意这是单向归一:不同名但同小写的文件会碰撞,所以别直接拿转换结果当唯一键。

三是处理土耳其语内容时的正确性校验。这是本页最不可替代的场景:土耳其语的标准做法是 i → İ、I → ı,与英语的 i → i、I → i 完全不同。如果你的系统按英语规则处理土耳其语文本,用户搜不到自己输入的词。切到 tr 档位就能立刻看到差异。

四是规范化数据库主键与搜索索引。入库前统一小写,查询时也统一小写,Apple 和 apple 就能互相匹配。代价是原始大小写信息丢失,展示时要另存一份原始值。

本页不做这些事:不做首字母大写或逐词大写(那是格式整形,用「句子大小写转换器」或「Capitalize开头字母工具」);不做驼峰或下划线命名转换;不做全大写——那正好是本站已完成的「字母转大写」页,与本页互为反向操作,注意区分两者的 locale 陷阱差异:转大写时 ß 会展开成 SS,转小写时 ß 保持不变。要按字符种类统计请用「字母统计」与「字符数统计工具」。

原理:toLowerCase 到底按什么规则映射

调用的是 String.prototype.toLowerCase(),规则由 Unicode 标准定义,不是简单的 ASCII 表。ECMAScript 规范规定它使用 Unicode Default Case Conversion 算法,加上 toLowerCase 的上下文规则。这个算法把每个码点查一张 Unicode 数据表,表里定义了三类映射:单字符映射(A → a)、无映射(保持原样)、条件映射(依赖上下文,例如希腊语的最终 sigma)。关键取舍:用 Unicode 表而不是 charCodeAt 加 32 的做法,因为后者只对 ASCII 的 A-Z 有效,遇到 Ä、Σ、İ 会完全错。代价是转换结果不保证长度不变——有的映射会 1 个码点变 2 个。

第一条要记住的规则:土耳其语的 I 有三个不同的写法,toLowerCase() 与 toLocaleLowerCase('tr') 对它们的处理完全相反。三个码位是:I(U+0049,拉丁大写 I)、ı(U+0131,无点小写 i,土耳其语特有)、İ(U+0130,带点大写 I,土耳其语特有)。默认映射下:'I'.toLowerCase() 得 'i'(U+0069);而 'I'.toLocaleLowerCase('tr') 得 'ı'(U+0131)。反过来 'İ'.toLowerCase() 得两个码点:U+0069(i)加 U+0307(组合尖音符),显示成 i̇——因为 Unicode 没有「带尖音符的小写 i」这个预组合字符,只能用「基础字母 + 组合符」两码位表示。这是默认模式下码点数增加的唯一常见原因。

第二条:ß 转小写不变,转大写才展开。'ß'(U+00DF)的小写就是它自己,toLowerCase() 原样返回。但 toUpperCase() 会把它展开成 两个码点 SS(因为德语传统写法与新正字法允许 ss 代替 ß)。所以 'STRASSE'.toLowerCase() 得 'strasse',与 'straße' 不等价——这在做小写归一去重时是个真实的坑。反方向也有一条:大写形态的 ẞ(U+1E9E,2017 年才加入 Unicode 的 capital sharp s)转小写会正确地回到 ß(U+00DF),单码点。

第三条:希腊语的 Σ(U+03A3)是上下文相关的。它在词首或词中转成 σ(U+03C3,普通 sigma),在词尾转成 ς(U+03C2,最终 sigma)。实测:'Σ'.toLowerCase() 单独一个字母时是 σ(没有前文,算词首);'ΟΔΟΣ' 转成 οδος,末尾的 Σ 变成 ς;而 'ΣΑ' 转成 σα,开头是 σ。判定依据是 Unicode SpecialCasing.txt 的 Final_Sigma 条件:Σ 前若有字母、且后面不是字母时视为词尾。所以「同一个字符在词中位置不同结果不同」,这是补角式大小写转换里唯一的上下文依赖项。希腊语 locale(el)不改变这条规则——它本来就是 Unicode 默认行为。

第四条:JavaScript 字符串是 UTF-16,所以「长度」和「字符数」不是一回事。按 ECMAScript 规范,x.length 返回的是 UTF-16 码元个数,而 [...x].length 用字符串迭代器逐个 码点走。两者在遇到补充平面字符时不同:'😀'(U+1F600)在 UTF-16 里是一对代理 D83D DE00,算 2 个码元、1 个码点。所以本示例的输入是 73 码元 / 72 码点,差的正是那个表情。emoji 与汉字都不受 toLowerCase() 影响——它们在 Unicode 大小写表里没有映射,原样返回。

第五条:toLocaleLowerCase() 的参数决定用哪套区域规则。'HELLO'.toLocaleLowerCase('tr') 仍是 hello(E 之类不受土耳其语规则影响),说明区域设置只影响那几个特例字母,不是整体换表。但要注意 'I' 在英语里是常用字母,'i' 也是;而土耳其语里 i 才是常用字母、ı 是罕见字母——所以土耳其语用户输入的 I 必须转成 ı 才对得上,这正是要切 tr 的原因。az(阿塞拜疆语)行为与土耳其语一致,实测 'I'.toLocaleLowerCase('az') 同样得 ı。

NFC 归一化为什么放在转换之前。Unicode 的同一字符有两种表示:预组合的 é(U+00E9,单码点)与分解的 e + U+0301(组合尖音符,两码点)。两者在屏幕上完全一样,但码点数不同、字节数不同、字符串比较结果也不同。做小写归一前先 normalize('NFC'),可以避免「看起来一样的两份文本转完还是不一样」。本页默认勾选这一项;本示例文本本身已是 NFC 形式,勾不勾结果相同,但对你从 Word 或 macOS 复制的文本通常有影响。

完整示例(数值均为实测)

输入框里预置的 5 行示例是:

行输入默认模式输出tr 模式输出
1ORDER #A-1024order #a-1024order #a-1024
2İstanbul ıspartai̇stanbul ıspartaistanbul ısparta
3ΟΔΟΣ ΣΟΦΟΣοδος σοφοςοδος σοφος
4Straße ẞ 中文原样straße ß 中文原样straße ß 中文原样
5😀 MIXED Case 123😀 mixed case 123😀 mıxed case 123

五种口径的实测数字:输入 73 码元 / 72 码点;默认模式输出 74 码元 / 73 码点(码点数 +1);tr 模式输出 73 码元 / 72 码点(不变)。5 行全部发生变化(第 4 行只改了 ẞ → ß 一处)。输入含 13 个 ASCII 大写字母、4 个汉字;24 个码点发生了大小写映射,48 个码点没有任何映射因而原样保留。

三行值得逐位看:

  • 第 2 行:İ 在默认模式下变成 i 加组合尖音符(码位 U+0069 U+0307),这是码点数 +1 的来源;切到 tr 后 İ 转成 i,码位数守恒。注意 ısparta 里的无点 ı(U+0131)两种模式都不变——它本来就是小写。
  • 第 3 行:ΟΔΟΣ ΣΟΦΟΣ 转成 οδος σοφος,码位序列是 U+03BF U+03B4 U+03BF U+03C2 空格 U+03C3 U+03BF U+03C6 U+03BF U+03C2。两个词末尾的 Σ 都变成了 ς(U+03C2),而第二个词开头的 Σ 变成 σ(U+03C3)——同一个字符,因为词中位置不同而结果不同。
  • 第 5 行:😀 前后完全不变(它没有大小写映射),MIXED 与 Case 正常转小写;tr 模式下 MIXED → mıxed——把 I 转成了无点的 ı。这一行最能说明为什么英语内容不能切 tr。

最后看 ß 的对照:'Straße ẞ' 在本工具(转小写)下是 'straße ß',长度不变;只有在转大写时 'ß' 才会展开成 'SS'(1 码点变 2 码点)。所以拿本页结果去和「字母转大写」页的结果对照看,能直观体会到两个方向的长度行为不对称。

十二组码位映射与输入范围对照

下表列出最值得注意的映射,「码点数变化」列是重点——非 −1 的行说明转换后长度与字符数不再相等:

输入码位toLowerCase 输出输出码位码点数变化说明
IU+0049iU+00690默认映射;tr 下改成 ı
ıU+0131ıU+01310无点小写 i,两种模式都不变
İU+0130i̇U+0069 U+0307+11 码点变 2 码点,唯一会变长的常见情况
I(tr)U+0049ıU+01310toLocaleLowerCase('tr') 的结果
ßU+00DFßU+00DF0转小写不变;转大写才变 SS
ẞU+1E9EßU+00DF0大写形态正确回到 ß
ΣU+03A3σU+03C30单独出现按词首处理
ΟΔΟΣ—οδος…U+03C20词尾 Σ → 最终 sigma ς
ΣΑ—σαU+03C3…0词首 Σ → 普通 sigma σ
DžU+01C5džU+01C60标题大小写字母转小写斜体
KU+212AkU+006B0开尔文符号 K(U+212A),是独立符号码位,同样映射到小写 k
😀 中文—😀 中文—0无大小写映射,原样保留

输入范围与边界:上限 20 万 UTF-16 码元(输入框的 maxlength,按码元计)。换成实际能装多少:纯 ASCII 可装 20 万字符,纯汉字约 10 万个(每个汉字 1 码元、UTF-8 3 字节),纯 emoji 只能装 10 万个(每个 2 码元)。按 80 字符一行算,ASCII 文本约 2500 行。20 万码元做一次 toLowerCase 是毫秒级操作。

边界、精度与常见误用

  • 码点数可能增加,不会减少。Unicode 的大小写映射里,把 1 个码点变成多个码点的情况确实存在(实测 İ → U+0069 U+0307,+1)。反向不会出现——没有任何小写形式会映射成 2 个码点。所以输出长度 ≥ 输入长度(按码点算),这一点在做「截断到 N 字符」的业务逻辑时要记住:转换后再截断,和先截断再转换,结果长度不一样。
  • 「已是无小写」的输入仍然会原样输出,不报错。输入 hello 得 hello,码点变化 0,映射数 0。这是正确行为不是故障——toLowerCase() 是幂等函数(f(f(x)) = f(x)),对已经全小的文本再转一次结果不变。页面上「已是无小写,保持不变」这项显示的就是无映射的码点数。
  • 空文本与纯空白是合法输入。''.toLowerCase() 长度 0,' '.toLowerCase() 长度 3(空白没有大小写映射,原样保留)。本页不会因为空输入报错——这是文本转换工具,不是数值计算器,没有「除零」之类的失败模式。
  • 唯一会报错的路径是超上限。超过 20 万码元时页面给出具体提示(说明是页面保护而非算法限制),且不会静默截断已有内容。TextEncoder 在极旧浏览器上不存在,但本页主路径只用 toLowerCase 与 normalize,两个都是 ES5 / ES6 标准方法,兼容性很好。
  • 增补平面字符在多数语言里没有大小写映射。数学字母数字区(U+1D400 起,比如「𝔘」)实测不发生变化——这些字符在 Unicode 里没有登记大小写关系。这一点常被误解为「字体里的字母应该能转换」:字体只是把普通字母画成了花体,码位仍是普通字母;而真正使用数学字母区码位的文本才不转换。emoji 同理。
  • normalize('NFC') 会改变码点数,所以它必须排在转换之前。NFC 把分解形式合并成预组合形式(如 e+U+0301 → U+00E9),码点数减少;反过来 NFD 会增加。本页的顺序是先 NFC 归一、再转小写,这样输出是规范形式。反过来做(小写后再 NFC)也能得到同样结果,但先做归一化对输入更友好——能同时处理「分解形式的 İ」这种更麻烦的情况。
  • 全角字母会转成半角小写。A(U+FF21 全角大写 A)实测转成 a(U+FF41 仍是全角)——全角不会变半角,因为 Unicode 把全角字母登记为独立码位并各自定义了小写映射。要全角转半角必须用「全角半角转换」,本页不做这件事。
  • 只影响大小写,不影响其它任何字符。标点、数字、空格、emoji、汉字全部原样保留。想同时做去空格、删标点这类清洗,请用「删除多余空格」或「删除标点工具」,把它们和大小写转换混在一起会让归一化结果难以追溯。

常见问题

为什么转小写之后字符串反而变长了?

因为 Unicode 的映射表里存在「1 个码点 → 2 个码点」的情况。实测最常见的就是土耳其语的 İ(U+0130,带点大写 I):它的小写是 i 加组合尖音符(U+0307),显示成 i̇,因为 Unicode 里没有「带尖音符的小写 i」这个预组合字符。本页示例文本在默认模式下码点数从 72 增到 73,正是这一个字符造成的。切到 tr 模式就不会变长,因为那时 İ 直接映射到单码点的 i。

ß 转小写为什么不变成 SS?

因为 ß(U+00DF)本来就是小写字母,toLowerCase() 查表发现无需映射,原样返回。展开成 SS 的是反方向:toUpperCase() 会把 ß 扩成两个大写 S(2017 年前的 Unicode 甚至直接返回 SS 字符串)。这造成一个实际坑:'STRASSE' 转小写得 'strasse','straße' 转小写得 'straße',两者不相等——拿小写形式做去重键时,德语文本会漏掉一部分重复。要彻底归一得先把 ß 和 ẞ 统一替换成 ss。

希腊语里 Σ 转出来有时是 σ 有时是 ς,对吗?

对的,这是 Unicode SpecialCasing.txt 里的 Final_Sigma 条件规则:Σ 只在词尾转成最终 sigma ς(U+03C2),在词首或词中转成 σ(U+03C3)。实测 'ΟΔΟΣ' → 'οδος'(末尾是 ς),'ΣΑ' → 'σα'(开头是 σ),'Σ' 单独出现 → 'σ'(没有前文,按词首算)。所以转换结果依赖字符在词中的位置,这是 toLowerCase 唯一的上下文相关行为。希腊语 locale 档位不改变这条规则,它本来就是默认行为。

切到 tr 档位后 MIXED 变成了 mıxed,这是错了吗?

不是错,是土耳其语的正确规则。土耳其语把 i 当常用字母、ı(无点小写)当罕见字母,所以 toLocaleLowerCase('tr') 必须把 I 转成 ı 才能和用户输入的词对上。代价是英语内容会被改坏——本例第 5 行 MIXED 在 tr 下变成 mıxed,拿去和英语数据库比对就匹配不上了。所以只在处理土耳其语/阿塞拜疆语文本时切 tr,其它语言一律用默认档。az(阿塞拜疆语)行为与土耳其语一致。

为什么 x.length 和 [...x].length 差 1?

因为 JavaScript 字符串内部是 UTF-16,x.length 数的是码元,而 [...x] 用字符串迭代器逐个码点走。两者只在遇到补充平面字符(码位 > U+FFFF)时才不同。本页示例里有个 😀(U+1F600),它在 UTF-16 里是一对代理项 D83D DE00,所以算 2 个码元、1 个码点,输入的 73 码元对应 72 码点。汉字与 BMP 内的常用符号两者一致,所以纯中文文本里不会看到差异。统计「有几个字」时建议用码点口径。

全角字母会被转成半角吗?

不会。实测 A(U+FF21 全角大写 A)转成 a(U+FF41),仍是全角。Unicode 把全角字母登记为独立码位并各自定义了小写映射,所以「全角 A」的小写就是「全角 a」。要真正做全角转半角(涉及 U+FF01~U+FF5E 这一整段与 ASCII 的对应关系),请用「全角半角转换」工具。视觉上容易混淆,但码位上这是两回事。

转小写能用来做 URL 吗?有什么要注意的?

能,但本页只解决大小写这一件事,做 slug 还缺三步:空格要换成连字符、标点要删或转义、非 ASCII 要音译或直接丢弃。所以 /Product Name 转小写只是 /product name,仍然不是合法 URL。完整流程请用「URL别名生成器」或「Slugify字符串工具」。另外注意服务器差异:Linux 上的 Nginx 与 Apache默认区分大小写,改成小写路径后所有旧的大写链接会 404,必须同步做 301 跳转;Windows 与 macOS 的默认文件系统不区分,但生产环境的前置 CDN 往往区分。

我输入的文本会被上传或保存吗?

不会。转换完全由页面内的 toLowerCase() 与 normalize() 完成,没有任何 fetch 或 XMLHttpRequest 调用,也不写 localStorage;刷新页面后输入框回到那段 5 行预置示例。想自己确认,打开开发者工具的 Network 面板,输入文字时观察是否有请求发出——不会有。20 万码元的上限也纯粹是页面保护,与算法能力无关。

相关工具