页面里该显示的 <div> 被吞了,中文又变成一串数字
两次事故都不是「转义没做」。一次是把旧文档正文贴回页面,本该原样显示 <div> 的那行被浏览器当真标签吃掉;一次是从旧库导出评论,正文里每个中文都成了 &#20013; 这种 12 字符长串,看着像字符集坏了。修完只留一条规矩:先数清被编了几层,再决定动不动手。
这一轮把 HTML实体编解码 模板里的 htmlEncode、htmlDecode、html2js 三个函数逐字抽出来一条条喂。要量清三件事:全量档的产物长什么样、实体名表覆盖到第几名、只差一个字符的串会走哪条路。
encodeAll 一开,中文先进的是双重转义
不勾时它只动那五个特殊字符,其余一律透传,控制字符都不碰。勾上后多出的那段把所有非 ASCII 的 UTF-16 单元换成数字实体,顺序在这里翻了车:非 ASCII 的替换排在和号转义之前,刚拼出来的实体里那个和号又被下一轮全局替换编了一次。于是「中」的产物是字面 &#20013;,12 个字符,不是很多人以为的 中 单层形态。
那串符号不是数字实体本身,而是实体外面又套了一层转义。中文区码点全是五位,这档下每个汉字固定占 12 个字符。站内那篇 HTML实体编解码的坑:引号与尖括号 记的「库里存转义形态、渲染时再编一次」反复发作,根源就在这个顺序上。
实体名表字面 17 个键,命名分支真正走得到的只有 16 名
还原表字面写着 17 个键,命名那段的正则却只认「和号加纯字母再加分号」。表里 ' 这个键带数字,永远轮不到命名路径,它是由十进制那段还原的——命名表实际覆盖 16 名。除基础五件套外逐条喂一遍:
- 解成半角空格,码点 32——模板字面量写的就是普通空格,与标准里那个 160 不是同一字符;
- ©、®、™、' 照常还原;
- 两个破折号、省略号、四个引号方向全部命中。
这条最容易吃亏:解完看着正常,实际把「不换行」的属性丢了。反向用 encodeAll 编一个不换行空格,产出正好是 &#160;,两边对不上才知道 160 才是本尊。
容错清单:差一个字符就整段不认,而且不报错
| 输入(字面) | 这一页解出来 | 为什么 |
|---|---|---|
&、<(缺分号) | 原样 | 三段正则都要分号收尾,少一个字符整段不认 |
<、A | 原样 | 十六进制正则只认小写 x,命名正则不含 # |
&、< | 原样 | 正则命中,但查表区分大小写,查无就回填 |
&foo;、&amp; | 原样 | 字母段贪婪吞成 ampamp,查无此名 |
&& | && | 第一个和号后面不是字母,第二个照常还原 |
共同点是安静:不报错、不提示,符号原样留在页面上。
三段替换是串行的:一次调用能吃掉两层
htmlDecode 依次跑命名、十进制、十六进制三段,每段都会扫前一段刚拼出来的新串。把 &#60;(9 个字符)喂进去,一次就得到 <:命名段还原成和号、拼出 <,十进制段紧跟着吞成尖括号。对照 &lt; 只剥一层就停,因为命名段不回看自己拼出的串。
数字实体开头的串因此比命名实体的多怕一次解码,往返就在这破口:12 个本来就长得像实体的串先编后解,11 个逐字符回到原样,唯一反例正是 <;缺分号的 < 反倒稳稳回来。
图 1:往返恒等数与唯一反例。
双重转义的字节账:每多编一轮净增 4 个字符
膨胀只在实体里的和号上:每编一轮它从 1 个字符变成 5 个,净增 4。从缺分号的串起手最干净:< 是 3 个字符,编一次 &lt 是 7 个,编两次 &amp;lt 是 11 个。反方向是等差减法:&amp;amp; 这种 13 字符的串,解一次剩 9、两次剩 5、三次剩 1,每次恰剥一层命名实体,第四次结果不再变——一个孤零零的和号就是解码的不动点。
图 2:编码方向与解码方向都等差。
统计行报的是 UTF-16 单元,不是字节
下面那行统计取的是 JS 串长,也就是 UTF-16 code unit。两行输入 中文abc 与 line2 报原始字符 12,同一串的 UTF-8 实字节是 16;一个 emoji 按 2 个单元计。编号也跟着漂:勾全量后那个 emoji 被拆成 &#55357;&#56832; 两段——正则不带 u 旗标,取的是代理值不是码点。
越界的数字实体不报错,它静默给你一个错字符
十进制段不做范围校验,parseInt 的结果直接交给按 65536 取模的构造函数:😀 解出码点 62976 的私用区字符,并不是那个 emoji;� 解出码点 0 的空字符。不报错也不原样保留:长度看着对,内容已换。闭环只在自家手里:那两段代理编号再解一次能拼回原字符,交给严格的标准解析器就是不合法引用。
HTML 转 JS:实测三种输出,列表页文案只写了两种
同一段两行输入(含插值、单引号、反斜杠、反引号,64 个 UTF-16 单元)在三个 type 下分别是 81、73、95 字符:模板那档最短,数组 join 最长。差别在转义面:模板档把插值符号防住,数组档不碰美元符与大括号,也完全不处理回车,回车被原样带进第一行的字符串字面量。下拉里三种都在,说明文案只提了拼接与模板两种。
遇到一串符号怎么下手
顺序是先数层再动手:把字面串复制进解码框,解一次看剩下什么,还剩一层就再解一次,剩下一个孤零零的和号就该停——< 那种一次吃两层的另说。存的时候放回原始字符、只在输出那一刻编一次,这两次事故才没再复发。
常见疑问
Q:勾了「编码所有非 ASCII」,页面为什么显示一串 &#20013; 而不是中文?
替换顺序的问题:数字实体里那个和号又被编了一次,产出天生是双层形态。
Q:  少一个分号为什么不还原,也不报错?
命名那段的正则要求分号收尾,缺了就整段不认、原样回填。
Q:解完之后为什么不换行空格撑不住了?
这一页把它还原成半角空格,码点 32,不是标准里的 160;空格看着还在,属性没了。
Q:12 个实体形输入先编后解,为什么不全部回得来?
11 个恒等,唯一反例是数字实体开头那位:三段替换串行,一次调用连剥两层。