雜記 › 技術筆記 › 文字↔ASCII 裡的「ASCII」其實是 UTF-8:1 個漢字吐出 3 個數,無分隔檔 100% 回不去

文字↔ASCII 裡的「ASCII」其實是 UTF-8:1 個漢字吐出 3 個數,無分隔檔 100% 回不去

頁面上寫著「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 碼」輸出
A1165
中13228 184 173
全形 A13239 188 161
Hi2272 105

「二進位」那檔同理:第 138 行 padStart(8, "0") 把每個位元組補滿 8 位,第 161 行的十進位不補零,7 就寫作 7。1 個漢字佔 3 組共 24 位,空格分隔時輸出長 26;那個 4 位元組的 emoji 是 4 組 32 位、35 字元。

ASCII 檔:數字個數 1 3 4 3 A 中 emoji 全形 A 65 228 184 173 240 159 152 128 239 188 161

圖 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 當成一個整數,18537 mod 256 得 105,正是 i。
  • 同一檔下 2 個換行 \n\n 只剩 1 個,那條混排串(A、中、emoji、全形 A、1)解出 U+0000;ASCII 側整串報錯 解码失败:无效的ASCII码 228184173。
可逆組數 6 6 6 0 空格 逗號 換行 無分隔 Hi 只剩 i

圖 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,只能單向走一遍。

📎 相關文章

171! 才溢出,23! 就已經算錯:階乘頁那句「0~1000 不會溢出」的實測邊界 2026-10-27 一個漢字只算 5 bits:密碼強度評估器的破解時間什麼時候變成宇宙壽命級別 2026-10-27 它把餘數為 0 的那一行也打給你:GCD 過程展示頁的步數、摺疊順序與 29 步斐波那契 2026-10-27 GIF 縮完尺寸只剩首格:一個檔名叫 .gif 的 PNG 檔案 2026-10-27 BMP 進、PNG 出:12342 位元組的位圖變成 281 位元組,檔名卻還寫著 .bmp 2026-10-27