前端知库

innerHTML、innerText、textContent 和 outerHTML 有什么区别?哪个有 XSS 风险?

textContent 读写纯文本,读取时包含 script、style 和隐藏元素里的文字,写入时字符串不会被当成 HTML。innerText 读的是渲染后用户看得到的文字,会考虑 CSS,读取会触发重排,写入时把换行变成 br。innerHTML 读写元素内部的 HTML,outerHTML 连元素自己一起,写入时字符串会被解析成 DOM,是 XSS 的注入点。插入的 script 不会执行,但 img 的 onerror 这类写法照样能执行代码。

参考答案

简答

假设页面上有 <div id="box">你好,<b>小明</b></div>。box.textContent 和 box.innerText 读到的都是文字“你好,小明”,box.innerHTML 读到的是带标签的 你好,<b>小明</b>,box.outerHTML 还要把外面的 <div id="box"> 也包含进去。

两个读文字的属性,差别在于看不看 CSS。textContent 只管 DOM,<script>、<style> 里的内容和 display: none 的文字都会读出来。innerText 读的是渲染后用户看得到的文字,接近用鼠标选中再复制的结果,隐藏的文字不读,所以读取时要先算好样式,会触发重排。

写入时差别更大。给 textContent 或 innerText 赋值,字符串只会变成文字,写 <b> 页面上就显示 <b>。给 innerHTML 或 outerHTML 赋值,字符串会被当成 HTML 解析成元素,这正是 XSS 的入口:<script> 虽然不会执行,但 <img src=x onerror=...> 里的代码会执行。

所以显示用户输入的纯文本时,用 textContent。确实要插入 HTML,先用 DOMPurify 这类库净化,再配合 Trusted Types 限制只能传入净化过的内容。还要注意 outerHTML 赋值会把元素本身替换掉,原来的变量仍然指向旧元素。

读取时各自返回什么

先看一段结构,里面有普通文字、样式、隐藏元素和换行:

HTML
<div id="box">
  <style>#box b { color: red; }</style>
  你好,<b>小明</b><br />
  <span style="display: none">隐藏文字</span>
</div>
JavaScript
box.textContent; // 包含 style 里的 CSS 文本、“你好,小明”和“隐藏文字”
box.innerText; // “你好,小明”,br 处有换行,不含 CSS 文本和隐藏文字
box.innerHTML; // 内部的 HTML 字符串,包括 style、b、br、span 标签
box.outerHTML; // 在 innerHTML 外面再包上 <div id="box">…</div>

为什么 textContent 会读到 CSS 文本?MDN 的解释是,textContent 获取所有元素的内容,包括 <script> 和 <style>,它只是把所有子文本节点拼起来,不关心页面上显示成什么样。注释和处理指令会被跳过。

innerText 正好相反,MDN 说它表示节点渲染后的文本,近似于用户选中内容复制到剪贴板得到的文字。所以它知道 <br> 是换行,会跳过隐藏元素,也会受 text-transform 这类样式影响。因为要考虑 CSS,读取 innerText 会触发重排,以拿到最新的计算样式,循环里频繁读取时要留意,重排的代价见《重排和重绘有什么区别?哪些操作会触发它们?》。元素本身没有渲染时,比如已经从文档移除或被隐藏,innerText 返回的就和 textContent 一样。

还有一个边界:对 document 或 doctype 读 textContent 返回 null,想拿整页文字要读 document.documentElement.textContent。

写入时谁会解析 HTML

四个属性都能赋值,但对字符串的处理不同。

textContent 赋值时,会移除元素的所有子节点,换成一个文本节点。字符串里写 <b> 也只是文字,不会变成元素。innerText 赋值也会替换所有子节点,区别是字符串里的换行会被转成 <br> 元素。

innerHTML 赋值时,字符串会交给 HTML 解析器,解析出来的元素替换掉原来的子节点。MDN 也因此不建议用 innerHTML 设置纯文本:它处理的是原始 HTML,有 XSS 风险,而且要调用 HTML 解析器,比 textContent 慢。

outerHTML 赋值会把元素自己连同后代一起替换掉。这里有两个容易踩的坑。一是替换以后,原来的变量还指向旧元素:

JavaScript
const p = document.querySelector('p');
p.outerHTML = '<div>替换后的内容</div>';
p.nodeName; // 还是 "P",页面上已经是 div 了

二是元素的父节点是 document 时不能设置,比如对 document.documentElement 赋值 outerHTML 会抛出 NoModificationAllowedError。

为什么 innerHTML 是 XSS 的入口

MDN 把 innerHTML 称为最常见的 XSS 注入途径之一。它确实会阻止通过它插入的 <script> 执行,但攻击者有很多别的办法:

JavaScript
const name = "<img src='x' onerror='alert(1)'>";
el.innerHTML = name; // 图片地址无效,onerror 里的代码会执行

outerHTML 的情况相同。所以只要字符串可能来自用户,比如评论内容、URL 参数,就不能直接交给这两个属性。

只是显示文字时,直接用 textContent,MDN 也是这样建议的。确实需要渲染用户提供的 HTML,比如富文本评论,要先用 DOMPurify 这类库去掉危险的标签和属性。更进一步,可以用 CSP 的 require-trusted-types-for 'script' 强制这些属性只接受 TrustedHTML 对象,这样漏掉的地方会直接报错,见《Trusted Types 会自动过滤所有危险 HTML 吗?》。XSS 的分类和整体防范见《XSS 有哪几种?前端怎样防范?》。

常见追问

React 和 Vue 里对应的是哪个?

Vue 文档写明,双大括号插值把数据当纯文本,不当 HTML,要插入 HTML 得用 v-html,它更新的就是元素的 innerHTML。React 对应的是 dangerouslySetInnerHTML,文档说它会覆盖 DOM 节点的 innerHTML。两者的文档都警告不要用在用户提供的内容上,确实要用时先净化。

只是想清空一个元素,用哪个?

el.textContent = '' 或 el.replaceChildren() 都可以,它们不需要解析 HTML。el.innerHTML = '' 也能清空,只是要经过 HTML 解析器。