页面上写着「ASCII 码」,一个汉字却吐出 3 个数
同事想把一段中文转成 ASCII 码,页面跳出 228 184 173 三个数;重粘一次,还是三个数。可 ASCII 表的上限只有 127,汉字压根不该有「ASCII 码」。第二轮更怪:Hi 按「文本↔二进制」转成 01001000 01101001,她嫌那个空格碍事,把分隔符切成「无分隔」,粘回去解码只剩一个 i。
把 文本编码转换 这页读一遍就通了:3 个模式底下是同一个编码器加三套切分规则,「1 个字符 = 1 个码」从来不成立。
只有一个编码器:TextEncoder 恒为 UTF-8
第 136 行与第 160 行写的都是 new TextEncoder().encode(text)。这个 API 不收编码参数,只有 UTF-8 一种输出,所以「一个字符吐几个数」由字节数决定:
| 输入 | 页面统计的字符数 | UTF-8 字节 | 「ASCII码」输出 |
|---|---|---|---|
A | 1 | 1 | 65 |
中 | 1 | 3 | 228 184 173 |
全角 A | 1 | 3 | 239 188 161 |
Hi | 2 | 2 | 72 105 |
「二进制」那档同理:第 138 行 padStart(8, "0") 把每字节补满 8 位,第 161 行的十进制不补零,7 就写作 7。1 个汉字占 3 组共 24 位,空格分隔时输出长 26;那个 4 字节的 emoji 是 4 组 32 位、35 字符。
图 1:标签「ASCII码」在模板第 102 行,产出的一直是 UTF-8 字节。config/tools.php 第 1180–1184 行另有 5 处文案在承诺这个词,15 条主张里 [一致] 7 条、[冲突] 8 条。
「无分隔」这一档,工具自己产出的编码 100% 回不去
分隔符只参与正向拼接,反向一律重新切 token:二进制反向第 144 行把所有非 0/1 的字符当空白,ASCII 反向第 166 行把所有非数字当空白。解码端并不会因此知道原串有几个字节。
- 空格 / 逗号 / 换行 3 档下,6 组源串正反两向逐字节相同,一共 48 次往返。
- 「无分隔」档 100% 不可逆:
Hi塌成 1 个i——第 148 行把0100100001101001当成一整个整数,18537mod 256 得105,正是i。 - 同一档下 2 个换行
\n\n只剩 1 个,那条混排串(A、中、emoji、全角A、1)解出 U+0000;ASCII 侧整串报错解码失败:无效的ASCII码 228184173。
图 2:源串 6 组、4 档分隔符下的可逆组数。串越长越危险——第 148 行 parseInt(parts[i], 2) 返回 double,安全整数上限 9007199254740991,越界后连「取末字节」都算错:那条混排串的 96 位输出 mod 256 得 0,真实末字节是 49。
同一个越界值,两条反向路径两套口径
ASCII 反向第 170 行有 n > 255 校验,命中就整串放弃并回显第一个越界 token,256、257、65536 都走这一句。二进制反向第 148 行什么都不校验,值塞进 Uint8Array 靠取模截断:511 存成 255、300 存成 44。所以 111111111 在二进制侧变成 U+FFFD,在 ASCII 侧直接报错。
模板写了 4 条错误出口,实测 3 条不可达:TextDecoder 没传 {fatal: true},非法序列只产出 U+FFFD 不抛异常,两个 catch 是死代码;负号在第 166 行就被当成分隔符吃掉,-5 解成 U+0005。「转换失败:」在整套矩阵里出现 0 次。
全角↔半角:94 个码点会变,146 个原样返回
这一档不是通用的全角半角互转,而是两段固定偏移的码点算术。全角转半角的正则只匹配 U+FF00–U+FFEF,真正被改写的是 U+FF01–U+FF5E 的 94 个码点,同块另外 146 个原样返回,。 和 、 压根不在正则里。
最容易被当成 bug 的是全角空格:第 184 行写着 if (code === 0x3000) return " ";,可第 182 行的正则不含 U+3000,分支永远不执行。实测 ! A → ! A,字符数 3 不变,字节数从 9 掉到 5。
半角转全角覆盖 U+0020–U+007E 共 95 个码点,第 194 行把空格特判成 U+3000。两个方向 .length 都恒定,变的只有字节数:ABC 12! 的 7 字符 21 字节变成 ABC 12! 的 7 字符 9 字节,U+3000 那 3 个字节省不下来。按字节反推编码是另一件事,见 GBK 与 UTF-8 的判定。
没有十六进制档:别的编码粘进来只会腐坏
模板里 toString(16) 出现 0 次,0b、0x、\u 三种前缀一个都不加,所以拿不到 e4 b8 ad;也没有 Base64 与 HTML 实体的分支。粘进别的编码不报错,只把字母当分隔符删掉:
- 十六进制串
48 65 6C 6C 6F走 ASCII 反向得到0A加 3 个 U+0006;URL 编码%48%65%6C%6C%6F结果一模一样。 \u4e2d解成 U+0004 U+0002;要\u形式的中文得走 中文转 Unicode 那一篇。- 单个
228反向是 U+FFFD,因为0xE4是 UTF-8 首字节、后面缺延续字节;228 184 173一起粘回去才得到中——「ASCII」标签下面其实是个 UTF-8 解码器。
统计条数的是码元,但输入确实没有上限
第 229 行用 .length 填「输入字符」,那是 UTF-16 码元数:1 个 emoji 报 输入字符:2,混排串报 6 字符 / 12 字节,真实码点只有 5。
第 37 行与第 44 行两个 textarea 都没有 maxlength,JS 里也没有长度闸门:10000 个 A 的 ASCII 输出 29999 字符、二进制输出 89999 字符,Node v24.19.0 纯计算耗时 1.0 ms 与 2.4 ms;100000 字符是 6.3 ms 与 18.4 ms,两向来逐字节相同、没有截断。浏览器排版体验本机不可测,这两个 ms 只当纯计算量级。
三个绕不过去的疑问
Q:「文本↔ASCII」能不能用来转中文?
拿到的不是 ASCII,而是 UTF-8 字节序列;真要严格 ASCII,得先确认每个字符都在 127 以内。
Q:怎么保证编码还贴得回原文?
分隔符在空格、逗号、换行里任选,别用「无分隔」——那一档连工具自己产出的编码都还原不回来。反向也只认这套格式,别的编码串粘进来不会报错,只会静默变成控制字符。
Q:全角空格为什么转不成半角?
第 184 行要处理 U+3000,可第 182 行的正则只覆盖 U+FF00–U+FFEF,分支不可达;半角空格转全角时反而被第 194 行映射成 U+3000,只能单向走一遍。