頁面上寫著「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,只能單向走一遍。