雜記 › 技術筆記 › encodeAll 一開,中文先變成 中:HTML 實體表覆蓋與解碼容錯實測

encodeAll 一開,中文先變成 中:HTML 實體表覆蓋與解碼容錯實測

勾上「編碼所有非 ASCII」後中文產出的是雙層形態 中;解碼表字面 17 鍵只走得到 16 名,&nbsp 還原成半形空格。缺分號、大寫 X 一律原樣,一次呼叫還能連剝兩層。

頁面裡該顯示的 <div> 被吞了,中文又變成一串數字

兩次事故都不是「轉義沒做」。一次是把舊文件正文貼回頁面,本該原樣顯示 <div> 的那行被瀏覽器當真標籤吃掉;一次是從舊庫匯出評論,正文裡每個中文都成了 &amp;#20013; 這種 12 字元長串,看著像字元集壞了。修完只留一條規矩:先數清被編了幾層,再決定動不動手。

這一輪把 HTML實體編解碼 模板裡的 htmlEncode、htmlDecode、html2js 三個函式逐字抽出來一條條餵。要量清三件事:全量檔的產物長什麼樣、實體名表覆蓋到第幾名、只差一個字元的串會走哪條路。

encodeAll 一開,中文先進的是雙重轉義

不勾時它只動那五個特殊字元,其餘一律透傳,控制字元都不碰。勾上後多出的那段把所有非 ASCII 的 UTF-16 單元換成數字實體,順序在這裡翻了車:非 ASCII 的替換排在和號轉義之前,剛拼出來的實體裡那個和號又被下一輪全域替換編了一次。於是「中」的產物是字面 &amp;#20013;,12 個字元,不是很多人以為的 &#20013; 單層形態。

那串符號不是數字實體本身,而是實體外面又套了一層轉義。中文區碼點全是五位,這檔下每個漢字固定佔 12 個字元。站內那篇 HTML實體編解碼的坑:引號與尖括號 記的「庫裡存轉義形態、渲染時再編一次」反覆發作,根源就在這個順序上。

實體名表字面 17 個鍵,命名分支真正走得到的只有 16 名

還原表字面寫著 17 個鍵,命名那段的正則卻只認「和號加純字母再加分號」。表裡 &#39; 這個鍵帶數字,永遠輪不到命名路徑,它是由十進制那段還原的——命名表實際覆蓋 16 名。除基礎五件套外逐條餵一遍:

  • &nbsp; 解成半形空格,碼點 32——模板字面量寫的就是普通空格,與標準裡那個 160 不是同一字元;
  • &copy;、&reg;、&trade;、&apos; 照常還原;
  • 兩個破折號、省略號、四個引號方向全部命中。

&nbsp; 這條最容易吃虧:解完看著正常,實際把「不換行」的屬性丟了。反向用 encodeAll 編一個不換行空格,產出正好是 &amp;#160;,兩邊對不上才知道 160 才是本尊。

容錯清單:差一個字元就整段不認,而且不報錯

輸入(字面)這一頁解出來為什麼
&amp、&#60(缺分號)原樣三段正則都要分號收尾,少一個字元整段不認
&#X3C;、&#X41;原樣十六進制正則只認小寫 x,命名正則不含 #
&AMP;、&LT;原樣正則命中,但查表區分大小寫,查無就回填
&foo;、&ampamp;原樣字母段貪婪吞成 ampamp,查無此名
&&amp;&&第一個和號後面不是字母,第二個照常還原

共同點是安靜:不報錯、不提示,符號原樣留在頁面上。

三段替換是串行的:一次呼叫能吃掉兩層

htmlDecode 依序跑命名、十進制、十六進制三段,每段都會掃前一段剛拼出來的新串。把 &amp;#60;(9 個字元)餵進去,一次就得到 <:命名段還原成和號、拼出 &#60;,十進制段緊跟著吞成尖括號。對照 &amp;lt; 只剝一層就停,因為命名段不回看自己拼出的串。

數字實體開頭的串因此比命名實體的多怕一次解碼,往返就在這破口:12 個本來就長得像實體的串先編後解,11 個逐字元回到原樣,唯一反例正是 &#60;;缺分號的 &lt 反倒穩穩回來。

12 個實體形輸入先編後解,幾個回到原樣 逐字元相等 11 個 級聯剝兩層 1 個,數字實體開頭那位 輸入 12 個 · 一次呼叫跨 2 段 · 反例串長 9

圖 1:往返恆等數與唯一反例。

雙重轉義的位元組帳:每多編一輪淨增 4 個字元

膨脹只在實體裡的和號上:每編一輪它從 1 個字元變成 5 個,淨增 4。從缺分號的串起手最乾淨:&lt 是 3 個字元,編一次 &amp;lt 是 7 個,編兩次 &amp;amp;lt 是 11 個。反方向是等差減法:&amp;amp;amp; 這種 13 字元的串,解一次剩 9、兩次剩 5、三次剩 1,每次恰剝一層命名實體,第四次結果不再變——一個孤零零的和號就是解碼的不動點。

同一串:往上每輪加 4,往下每次減 4 起點 3 字元 編 1 次 7 字元 編 2 次 11 字元 13 字元起 13 → 9 → 5 → 1 再解一次 1 字元,不動點

圖 2:編碼方向與解碼方向都等差。

統計行報的是 UTF-16 單元,不是位元組

下面那行統計取的是 JS 串長,也就是 UTF-16 code unit。兩行輸入 中文abc 與 line2 報原始字元 12,同一串的 UTF-8 實位元組是 16;一個 emoji 按 2 個單元計。編號也跟著漂:勾全量後那個 emoji 被拆成 &amp;#55357;&amp;#56832; 兩段——正則不帶 u 旗標,取的是代理值不是碼點。

越界的數字實體不報錯,它靜默給你一個錯字元

十進制段不做範圍校驗,parseInt 的結果直接交給按 65536 取模的建構函式:&#128512; 解出碼點 62976 的私用區字元,並不是那個 emoji;&#1114112; 解出碼點 0 的空字元。不報錯也不原樣保留:長度看著對,內容已換。閉環只在自家手裡:那兩段代理編號再解一次能拼回原字元,交給嚴格的標準解析器就是不合法引用。

HTML 轉 JS:實測三種輸出,列表頁文案只寫了兩種

同一段兩行輸入(含插值、單引號、反斜線、反引號,64 個 UTF-16 單元)在三個 type 下分別是 81、73、95 字元:模板那檔最短,陣列 join 最長。差別在轉義面:模板檔把插值符號防住,陣列檔不碰美元符與大括號,也完全不處理回車,回車被原樣帶進第一行的字串字面量。下拉裡三種都在,說明文案只提了拼接與模板兩種。

遇到一串符號怎麼下手

順序是先數層再動手:把字面串複製進解碼框,解一次看剩下什麼,還剩一層就再解一次,剩下一個孤零零的和號就該停——&#60; 那種一次吃兩層的另說。存的時候放回原始字元、只在輸出那刻編一次,這兩次事故才沒再復發。

常見疑問

Q:勾了「編碼所有非 ASCII」,頁面為什麼顯示一串 &amp;#20013; 而不是中文?

替換順序的問題:數字實體裡那個和號又被編了一次,產出天生是雙層形態。

Q:&nbsp 少一個分號為什麼不還原,也不報錯?

命名那段的正則要求分號收尾,缺了就整段不認、原樣回填。

Q:解完之後為什麼不換行空格撐不住了?

這一頁把它還原成半形空格,碼點 32,不是標準裡的 160;空格看著還在,屬性沒了。

Q:12 個實體形輸入先編後解,為什麼不全部回得來?

11 個恆等,唯一反例是數字實體開頭那位:三段替換串行,一次呼叫連剝兩層。

📎 相關文章

1 GB 文件由 1024 塊拼成:任意大小文件生成的分塊節奏、取消邊界與三種填充位元組 2026-11-04 文件名提取的三處邊界:結尾斜槓只兜一格、查詢串整塊留下、空行靜默消失 2026-11-04 HTML 美化與壓縮不是互逆:10 個樣本的往返位元組帳實測 2026-11-04 貼上 100 行分數,第 45 行被悄悄算成 1/4:約分這頁的負號、空行和大數各歸誰管 2026-11-03 把 3. 起步貼進 Markdown 轉 HTML,為什麼有序列表的編號只認第一個數 2026-11-03