JS中innerHTML拼接与createElement创建元素的优劣对比

作者:小圆点时间:2026-11-03 14:06:20浏览:346

深入对比JavaScript中innerHTML拼接与document.createElement创建DOM元素的优缺点、底层解析原理、XSS安全风险、重绘重排开销及现代Web开发选型实践。

JavaScript 核心 DOM API 性能与安全 阅读时间:约 8 分钟
JS中innerHTML拼接与createElement创建元素的优劣对比
图 0:innerHTML 字符串解析机制与 createElement 节点对象机制速览
在原生 JavaScript 页面开发中,动态生成并插入 DOM 元素主要有两条路径:一是通过 innerHTML 属性解析 HTML 模板字符串,二是通过 document.createElement 在内存中直接构建节点对象。核心结论在于:innerHTML 凭借模板直观、代码紧凑的优势,适合用于单次渲染结构简单、静态且经过安全校验的无状态内容;而createElement(配合 DocumentFragment)在处理包含动态事件绑定、需要维持节点引用、频繁局部更新或承载不可信外部输入的复杂交互场景下,具备绝对的安全防护(天然防 XSS)与精确的重绘控制能力。两者各有明确的适用边界,脱离业务场景谈优劣并不科学。

一、底层工作原理与浏览器执行链路

理解两者差异的前提,是清楚浏览器在处理“一段 HTML 字符串”与“一个 JS 节点对象”时的底层链路区别。两者的性能损耗和行为副作用,本质上全部源自执行路径的不同。

1. innerHTML:基于字符串的 HTML 词法解析链路

当给容器元素的 innerHTML 赋值时,JavaScript 引擎并不会立即在内存中生成元素,而是将任务交还给浏览器的 HTML 解析器(HTML Parser)。解析器对字符串进行分词(Tokenization)、构建临时的 DOM 子树,然后替换容器原有的子节点。

这一过程存在一个关键副作用:如果容器原本已经包含子节点,赋值操作会将旧子节点全量销毁重建。这不仅带来额外的垃圾回收开销,还会导致原有子节点上通过 addEventListener 绑定的所有事件监听器永久失效,并且正在输入的输入框焦点、播放中的音频/视频播放进度等 DOM 状态全部丢失。

2. createElement:原生节点对象的直接实例化

document.createElement(tagName) 是标准的 W3C DOM API。调用该方法时,JavaScript 引擎跨越语言边界直接在底层 C++ DOM 树中实例化对应的元素对象(如 HTMLDivElement)。

此时新节点仅存在于内存中,尚未挂载到文档树。开发者可以直接以对象属性的方式操作它(例如配置属性、修改样式类、挂载事件监听),最后通过 appendChild 或 insertBefore 插入指定位置。原有容器内的其他子节点不受任何干扰,事件和引用完好无损。

JS中innerHTML拼接与createElement创建元素的优劣对比
图 1:innerHTML 解析链路与 createElement 节点构建链路对比

二、核心维度的深度对比

在实际工程落地中,开发者通常围绕安全性、事件与状态保持、渲染吞吐性能以及开发维护效率四个关键维度进行技术权衡。

1. 安全性对比:XSS 注入风险与天然免疫

这是两者最重要的安全分水岭。现代 Web 应用不可避免地需要展示来自 URL 查询参数、用户评论、后端 API 或本地缓存的动态数据。

  • innerHTML 的注入隐患:如果直接使用模板字符串拼接未经清洗的外部数据,攻击者注入的恶意 HTML 片段(如带有属性脚本的标签)会被浏览器 HTML 解析器直接执行,导致严重的 DOM-based XSS 漏洞。
  • createElement 的天然免疫:通过 createElement 创建元素后,若使用 element.textContent 或 element.innerText 填入文本,浏览器会将其直接作为文本节点(TextNode)处理,完全跳过 HTML 词法解析阶段。即使用户输入包含完整的标签字符串,也只会被渲染为字面字符,无法执行恶意脚本。
JS中innerHTML拼接与createElement创建元素的优劣对比
图 2:不可信数据输入时两种方式的安全机制与执行结果差异
⚠️ 安全警报:为什么 script 标签在 innerHTML 中不执行依然不安全?
HTML5 规范虽然规定通过 innerHTML 插入的 <script> 标签不应执行,但攻击者可以通过带有内联错误处理事件的非 script 标签轻松绕过限制。例如:
<img src="invalid-url" onerror="alert(document.cookie)"> 或 <svg onload="alert(1)">。
这些标签在被解析插入 DOM 树后会立刻触发事件回调,危害等同于任意脚本执行。因此,未经过滤的数据严禁直接赋给 innerHTML。

2. 交互与状态:事件绑定与 DOM 节点引用

现代 Web 页面强调丰富的用户交互。如果在动态创建元素的同时需要为每个子项绑定点击、悬浮等交互事件,两者的代码组织与运行行为差异显著:

  • innerHTML 无法直接传递函数引用:字符串模板内只能写内联 HTML 属性(如 onclick="handleClick(1)"),这要求回调函数必须挂载在全局作用域(如 window)下,破坏了模块封装与闭包作用域。如果要在渲染后绑定,必须使用 querySelectorAll 再次遍历查询 DOM,产生不必要的二次查询开销。
  • createElement 支持精准闭包绑定:在创建节点对象的当前作用域内,可以直接调用 el.addEventListener('click', () => { ... })。事件函数可以直接捕获当前循环中的局部变量,且直接持有目标节点的引用,代码高度内聚。

3. 性能表现:重排重绘与解析开销

关于两者谁更快的讨论,必须限定具体的场景条件:

  • 单次大批量静态插入(innerHTML 略胜):当需要一次性将几千条数据渲染为静态表格或列表时,先在内存中完成完整的大字符串拼接,然后一次性赋值给 innerHTML,通常比连续调用几千次 createElement + appendChild 耗时更短。因为字符串解析是在浏览器底层高优化的 C++ 原生代码中集中完成的,减少了 JS 运行环境与 DOM 原生接口之间的跨上下文调用开销。
  • 循环增量追加(innerHTML += 灾难性劣化):如果在循环中写 container.innerHTML += '<li>' + item + '</li>',每一次迭代都会触发一次“读取旧 HTML → 字符串拼接 → 销毁旧节点 → 重新解析新字符串 → 整体重新挂载”的完整周期。时间复杂度从 O(N) 急剧恶化到 O(N²),同时触发大量昂贵的页面重排(Reflow)与重绘(Repaint)。
  • 局部更新与追加(createElement 占优):添加单个弹窗、卡片或在列表末尾插入单条数据时,createElement 只对新节点做局部插入,不会波及同级与父级既有节点,重排计算范围最小。

4. 开发效率与可读性

从代码编写体验来看,借助 ES6 模板字符串,innerHTML 可以用几乎与纯 HTML 一致的缩进和结构书写复杂嵌套组件,布局层级一目了然,初期开发速度极快。

相比之下,纯用 createElement 构建深层嵌套结构(如带有标题、头像、徽章、操作按钮的复杂卡片)需要编写大量中间变量与多层 appendChild,代码显得繁复冗长。但在 TypeScript 或大型工程中,面向对象的节点属性调用具备良好的类型提示与编译期检查,有利于重构与静态分析。

三、批量渲染的高性能实操规范

为了在实际开发中兼顾性能、安全与交互能力,以下分别给出两种方式的标准工业级写法。

实战规范一:DocumentFragment 批量挂载(推荐用于交互型列表)

DocumentFragment 是一个轻量级的虚拟文档节点。将新创建的子节点先追加到片段中,此时所有操作完全在内存中进行,页面零重排;最后将片段一次性追加至页面容器中,浏览器仅触发一次重排。

JS中innerHTML拼接与createElement创建元素的优劣对比
图 3:DocumentFragment 批量操作规范四步法流程
// 1. 准备待渲染数据
const userList = [
  { id: 101, name: "Alice", role: "工程师" },
  { id: 102, name: "Bob", role: "设计师" },
  { id: 103, name: "Charlie", role: "产品经理" }
];

const container = document.getElementById('user-list');

// 2. 第一步:创建内存文档片段容器
const fragment = document.createDocumentFragment();

// 3. 第二步与第三步:遍历数据,实例化节点、绑定属性并添加闭包事件
userList.forEach(user => {
  const item = document.createElement('li');
  item.className = 'user-card';
  item.dataset.userId = String(user.id);

  // 安全赋值纯文本,杜绝XSS风险
  const title = document.createElement('strong');
  title.textContent = user.name;

  const desc = document.createElement('span');
  desc.textContent = ` (${user.role})`;

  const btn = document.createElement('button');
  btn.textContent = '查看详情';
  // 直接利用闭包绑定当前作用域变量,不污染全局
  btn.addEventListener('click', () => {
    console.log(`正在查看用户: ${user.name}, ID: ${user.id}`);
  });

  item.appendChild(title);
  item.appendChild(desc);
  item.appendChild(btn);

  // 追加到内存片段,完全不触发文档回流
  fragment.appendChild(item);
});

// 4. 第四步:一次性挂载到真实DOM,仅触发一次Reflow
container.appendChild(fragment);

实战规范二:模板字符串一次性拼接与事件委托(适用于静态展示列表)

如果确定需要使用模板字符串的高效书写方式,应确保“单次赋值”避免循环性能陷阱,并通过“事件委托”解决事件绑定问题,同时对外部动态变量进行转义。

// 纯文本字符转义辅助函数,规避基础XSS注入
function escapeHtml(str) {
  return String(str)
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;');
}

const articles = [
  { id: 1, title: "JavaScript 性能调优", views: 2400 },
  { id: 2, title: "浏览器渲染原理剖析", views: 1850 }
];

const listContainer = document.getElementById('article-container');

// 在内存中一次性完成字符串拼接
const listHtml = articles.map(item => `
  <div class="article-item" data-id="${item.id}">
    <h4>${escapeHtml(item.title)}</h4>
    <span>浏览量: ${item.views}</span>
    <button class="btn-read" data-action="read">阅读全文</button>
  </div>
`).join('');

// 仅在循环外部做单次赋值
listContainer.innerHTML = listHtml;

// 利用事件委托机制在父容器上统一监听,无需二次遍历子节点
listContainer.addEventListener('click', (event) => {
  const target = event.target;
  if (target.matches('.btn-read')) {
    const parentCard = target.closest('.article-item');
    const articleId = parentCard.dataset.id;
    console.log(`阅读文章 ID: ${articleId}`);
  }
});

四、现代 Web 开发的补充方案

在纯原生开发中,除了传统的 innerHTML 与 createElement 之外,现代浏览器还提供了另外两个兼顾开发体验与运行效率的原生 API:

  • insertAdjacentHTML:如果要向容器追加或插入 HTML 字符串,但不希望像 innerHTML 那样重写整个父节点,可使用 container.insertAdjacentHTML('beforeend', htmlString)。该方法直接将字符串解析为节点插入指定相对位置(beforebegin, afterbegin, beforeend, afterend),不会销毁容器已有的其他子节点及其事件监听器,是替代 innerHTML += 的绝佳方案。
  • <template> 标签克隆:在 HTML 中预先定义 <template> 模板,在 JS 中通过 template.content.cloneNode(true) 快速复制。这样既获得了 HTML 的可读性与编辑便利,又获得了 DOM 节点对象的安全引用与事件绑定能力。

五、综合优劣势对比与选型决策指南

对比维度 innerHTML 字符串拼接 createElement 节点创建
核心机制 交付 HTML 解析器进行词法分析并重构子 DOM 树 直接在内存中实例化原生 Element 对象
XSS 安全性 低。未过滤输入极易导致脚本注入,必须配合转义 高。配合 textContent 天然免疫脚本注入
事件绑定能力 较差。只能写内联全局属性,或渲染后二次遍历绑定 优秀。支持直接添加闭包监听,天然保留上下文
已有状态保留 破坏性。重写时清空子节点事件、输入框焦点和媒体进度 安全。局部插入不影响既有子节点的任何状态
单次大批量性能 优秀。单次大字符串由底层 C++ 集中解析,速度较快 良好。需配合 DocumentFragment 减少重排开销
多次追加性能 极差(若使用 +=)。每次均触发全量反序列化与回流 优秀。支持原子化局部增量插入
代码直观度 高。模板语法嵌套清晰,结构一目了然 较低。多层嵌套时变量与追加语句较多
JS中innerHTML拼接与createElement创建元素的优劣对比
图 4:技术选型决策逻辑与全维度权衡总结
💡 决策速查指南
  1. 必须选 createElement:涉及用户公开输入的表单/评论展示(防 XSS);需要为列表项直接绑定复杂闭包交互;原有容器内已有其他动态组件且不能被刷新的场景。
  2. 推荐选 innerHTML:纯内部受信任的静态模板大段渲染;快速搭建原型或结构简单的无状态内容展示(配合单次赋值与事件委托)。
  3. 推荐现代折中方案:需要按模板字符串追加元素时,优先使用 insertAdjacentHTML;需要复用复杂 DOM 结构时,优先使用 <template> 结合 cloneNode。
作者最新文章
相关文章
更多