← HTML 表单:从控件到提交与校验 / <textarea> 的内容不是 HTML——RCDATA 解析 待审核 15 / 24
textarea · RCDATA

<textarea> 的内容不是 HTML——RCDATA 解析

HTML tokenizer 并非全程处于同一个模式。普通元素里它处于 data state,遇到 < 就尝试开启新标签;而 <textarea><title> 一进入就切换到 RCDATA state:此时 < 不再开启新标签,唯一能让它退出的是对应的 </textarea>;但 & 仍会进入 character reference 解码。这条规则解释了 <textarea> 几乎全部反直觉的解析行为。

1 · 同一段字符串,放进 div 与放进 textarea

下面把完全相同的一段源码,分别作为 innerHTML 赋给一个 <div> 和一个 <textarea>,对比两侧解析出的 DOM 有何不同:

读法div 一侧把 <b> 解析成了真实的 HTMLElement,因此 childElementCount 大于 0、粗体真正生效;textarea 一侧 childElementCount 恒为 0<b> 原样留在 .value 里作为字符,而 &amp; / &#128512; 这类 character reference 仍然被解码& / 😀。

2 · 经典技巧:用 textarea 解码 HTML 实体

「RCDATA 解码实体、但不解析标签」这一组合,催生了一个流传已久的技巧——把一串带实体的文本写入 textarea,再读取 .value 即得到解码后的纯文本;且因为标签不被解析,<img onerror> 这类内容也不会真正建出元素:

不应把它当作 sanitizer。 它只负责解码实体;若需净化用户 HTML,应使用专门的库(如 DOMPurify)。现代代码若只需解码实体,更直接的做法是 new DOMParser().parseFromString(s, 'text/html').documentElement.textContent,无需借道 textarea

3 · 序列化:.value 并不进入 outerHTML

解析是「字符串 → DOM」,序列化则是反向的「DOM → 字符串」。这里藏着 textarea 最隐蔽之处:它的 outerHTML 序列化的是子文本节点(即「默认值」),而非当前 .value——因此脚本执行 ta.value = '...' 之后,outerHTML 里那对标签之间往往是空的。下面把同一段文本分别走 div.textContenttextarea.valuetextarea.textContent 三条路径,当场对照:

至于「输入的字符到底是几个 code unit / 字素」,以及 maxlength 据此如何计数,见 maxlength 数的是 UTF-16 code unit 一页。