杂记 › 技术笔记 › 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