只要写过几年前端,肯定绕不开往页面上动态添加元素这回事。用append往容器里塞div,看起来就是一行代码的事,但真到项目里,字符串拼接、DOM对象、批量循环、事件绑定、样式兼容,每个环节都能踩出坑来。这篇文章我会从原生JS写到jQuery,把append添加div的常见写法、性能对比、以及那些“文档里查不到”的实战细节一次讲透。
先说清楚这篇文章适合谁看:刚入门的前端新手,被老师的$('div').append(...)教过、但不知道底层在干嘛;写了两三年业务、想系统查漏补缺的进阶开发者;还有那些在维护老项目、必须跟jQuery打交道的同学。文章不会有太高深的理论,全部围绕“用append添加div”这一个点,把原生和jQuery对照着讲,再补上实际项目中一定会碰到的性能、事件、选择器这些衍生问题。
1. 先分清两个“append”:原生JS和jQuery的本质差异
1.1 原生DOM里到底有没有append方法
很多人以为原生JS只有appendChild,其实浏览器早就提供了append方法。Element.append()是DOM规范里较新的接口,它和appendChild最直观的区别在于两点:
append可以同时添加多个节点和字符串,appendChild只能接收一个节点对象;append没有返回值,appendChild会返回被追加的节点。
看一段最直接的对比:
// 原生 appendChild const box = document.getElementById('box'); const div = document.createElement('div'); div.textContent = '我是子元素'; box.appendChild(div); // 原生 append const box2 = document.getElementById('box'); box2.append('可以直接塞字符串', document.createElement('span')); // appendChild 如果传字符串会报错 // box.appendChild('字符串') // TypeError这里有个实际场景很常用:你想在容器最后追加一段HTML文本,用append可以直接写:
box.append('<div class="item">动态添加</div>');结果会以Text节点形式插入,所以页面上能看到字符串内容,但它不是真正的div元素。如果你要的是一个真实可操作的div节点,就得用insertAdjacentHTML或者先createElement再append。
1.2 jQuery的append是一个家族方法
jQuery里的append要复杂得多,它背后是一整套操作集合。很多人只记住了$('#box').append(content)这种用法,其实jQuery围绕“往容器末尾加东西”设计了好几个方法,具体区别我来拆一下:
| 方法 | 作用位置 | 语义 |
|---|---|---|
.append(content) | 在容器内部末尾追加 | 把内容放到选中元素里面、最后面 |
.prepend(content) | 在容器内部开头追加 | 把内容放到选中元素里面、最前面 |
.appendTo(target) | 把选中元素追加到目标容器末尾 | 和append方向相反,是“我把自己塞进你里面” |
.prependTo(target) | 把选中元素追加到目标容器开头 | 同样是反向操作 |
.after(content) | 在选中元素后面插入 | 追加的是兄弟节点,不是子节点 |
.before(content) | 在选中元素前面插入 | 也是兄弟节点 |
看代码就清楚了:
// append:把div加到box里 $('#box').append('<div class="item">新来的</div>'); // appendTo:把新建的div加到box里 $('<div class="item">新来的</div>').appendTo('#box'); // after:加在box外面,作为哥哥后面的兄弟 $('#box').after('<div class="item">我是box后面</div>');这两个方向一定要分清。append是“以容器为主体,往里塞东西”,appendTo是“以内容为主体,把自己塞进容器”。老项目里经常能看到$('<div/>').appendTo('body')这种写法,就是典型的“创建完直接插入”风格。
1.3 为什么append可以传HTML字符串
jQuery的append能直接识别'<div class="item">内容</div>'这种字符串并创建真实DOM,本质是jQuery内部使用了innerHTML机制。这里有一个很多人没注意到的关键点:当append的字符串比较复杂时,jQuery会尝试提取核心HTML;但如果你的字符串是一个单纯的文本节点,jQuery会走文本创建逻辑。
我举个日常例子:
// 这种会正常解析成div $('#box').append('<div class="item">内容</div>'); // 这种会是纯文本,不会变成div $('#box').append('hello world');如果你把变量拼进去,一定要保证变量里没有意外的HTML标签。否则要么XSS风险,要么页面结构被莫名其妙地破坏。比如:
let userInput = '<img src=x onerror=alert(1)>'; $('#box').append('<div>' + userInput + '</div>');这种写法会把用户输入当成HTML解析,轻则样式错乱,重则脚本注入。基于安全考虑,只要内容里带用户数据,我建议要么用text()方法写入、要么先createElement再textContent赋值,尽量不要直接拼在HTML字符串里。
2. 六种最常见的添加div写法与选择思路
2.1 字符串拼接法:最快但最脆弱
最简单的写法就是把HTML片段拼成字符串直接塞进去,原生和jQuery都能做:
// 原生 document.getElementById('box').innerHTML += '<div class="item">新div</div>'; // 原生insertAdjacentHTML document.getElementById('box').insertAdjacentHTML('beforeend', '<div class="item">新div</div>'); // jQuery $('#box').append('<div class="item">新div</div>');innerHTML +=这个写法我一般不推荐,因为每次都会把整个容器的HTML序列化再重写,哪怕只加一个子元素,也会导致所有子元素被销毁重建。后果很直接:子元素上绑定的JS事件全部丢失、表单状态重置、图片重新加载。多个元素循环添加时性能惨不忍睹。相比之下insertAdjacentHTML和jQuery的append会好很多,前者是直接解析并插入新片段,不触碰原有DOM结构。
2.2 createElement方案:可操作但代码量大
用document.createElement创建div,再用appendChild或append插入,是原生JS的标准做法:
const div = document.createElement('div'); div.className = 'item'; div.id = 'item-' + Date.now(); div.textContent = '我是动态创建的div'; div.dataset.index = '1'; document.getElementById('box').appendChild(div);这种写法最大的好处是完全可控。字段一个个设置,事件直接在上面绑定,拿到的是真实DOM对象,后续修改也方便。缺点是代码量大,创建10个div就要写10遍。实际项目里一般配合map、forEach来做批量生成:
const data = ['苹果', '香蕉', '橘子']; const box = document.getElementById('box'); data.forEach((name, index) => { const div = document.createElement('div'); div.className = 'fruit-item'; div.textContent = name; div.dataset.index = index; box.appendChild(div); });2.3 insertAdjacentHTML:原生里的“小jQuery”
insertAdjacentHTML可能是原生API里最被低估的一个,它的用法非常接近jQuery的append:
const box = document.getElementById('box'); box.insertAdjacentHTML('beforeend', '<div class="item">尾部插入div</div>'); box.insertAdjacentHTML('afterbegin', '<div class="item">头部插入div</div>');四个位置参数:
| 参数 | 插入位置 |
|---|---|
beforebegin | 元素自身之前(兄弟位置) |
afterbegin | 元素内部、第一个子节点之前 |
beforeend | 元素内部、最后一个子节点之后 |
afterend | 元素自身之后(兄弟位置) |
这个API的好处就是不需要额外创建节点,直接把HTML字符串解析成DOM并插入,效率比innerHTML +=高得多,而且不破坏已有子元素。我封装动态列表的时候经常用它,代码简洁程度不输jQuery。
2.4 实际项目里怎么选
给个选择优先级供参考:
- 内容完全可信、且只是一次性插入:
insertAdjacentHTML或jQuery的append; - 需要绑定事件、绑定数据、后续频繁改样式:
createElement方案; - 循环插入大量元素:
DocumentFragment批量处理(第三部分细讲); - 维护老jQuery项目:优先用
.append(),但要注意和原生DOM交错使用时的坑。
3. 性能与踩坑:循环添加div时必须知道的事
3.1 循环里反复append为什么慢
这是个老生常谈但又值得反复强调的问题。每append一次,浏览器就要做一次布局计算和渲染更新。如果循环100次操作DOM,浏览器就要可能触发100次重排。体感是什么?页面卡顿、滚动掉帧、CPU飙升。
举个例子感受一下:
// 性能差:循环里反复append const box = document.getElementById('box'); for (let i = 0; i < 500; i++) { const div = document.createElement('div'); div.textContent = '第' + i + '个'; box.appendChild(div); }这种写法500个元素可能还好,一旦上到5000甚至几万个,页面会明显卡顿。以前我刚入行时不知道这个坑,循环几千个节点直接在页面上崩了一次,后来才知道要用DocumentFragment。
3.2 DocumentFragment:批量插入不卡顿
DocumentFragment可以理解为一个“虚拟容器”,它存在于内存中,不属于页面DOM树。你先往里面塞N个节点,最后只做一次真实DOM操作,把整个fragment插进容器。这样布局计算只触发一次,性能天差地别:
const box = document.getElementById('box'); const fragment = document.createDocumentFragment(); for (let i = 0; i < 1000; i++) { const div = document.createElement('div'); div.className = 'item'; div.textContent = '第' + i + '个div'; fragment.appendChild(div); } box.appendChild(fragment);重点在于:fragment插入后自身会清空,页面里得到的是它包含的那些div。这个特性能帮你省掉很多麻烦,比如不需要在插入后清空临时容器。jQuery时代也有类似的人为约定:用一个临时div当容器,全部append进去,最后一次性$('#box').append(tempDiv.childNodes)。
3.3 循环出来的div,一行两个如何选最后两个元素
这个需求听起来很奇怪,但实际项目中真的会遇到。比如商品列表每行展示两个div,最后一行如果不满两个,UI要求最后两个不加某种样式。热搜词里也出现了“循环出来的div 一行两个如何算最后两个元素”和“最后两个元素不加伪类”,说明不少人卡在CSS和JS双重限制上。
先明确一个概念:CSS伪类last-child、nth-last-child可以选最后两个元素,但当元素不是最后一个时,伪类的判断会错位。比如一行两个div的列表,你想让倒数第一个和倒数第二个不加边框,写成:
.item:last-child, .item:nth-last-child(2) { border: none; }这个写法在纯静态场景下是正确的,但如果最后一行只有两个div,而倒数第二个和倒数第三个是同一行的,那可就不一定是UI想要的效果了。真实的做法通常要结合请求回来的数据长度和当前行数来判断:
// 假设每行2个 data.forEach((item, index) => { const div = document.createElement('div'); div.className = 'item'; div.textContent = item.name; // 最后一行 const totalRows = Math.ceil(data.length / 2); const currentRow = Math.floor(index / 2) + 1; const isLastRow = currentRow === totalRows; // 最后一行第一个元素也不需要右边框,可以动态加类 if (isLastRow) { div.classList.add('last-row-item'); } list.appendChild(div); });这个方法虽然代码多,但判断逻辑完全可控,不会被CSS伪类的“最后一个”语义限制住。如果你想看纯CSS能不能解决,在结构完全规律的情况下,可以给容器设置每行宽度,然后直接用:nth-last-child(-n+2)选中倒数两个,配合:not(:nth-last-child(-n+2))处理前面的边框,也能应付,但前提是每个item宽度固定且确实是最后一行。我个人的经验是:涉及动态数据列表,能不用CSS伪类就别用,JS算好再打类名最稳。
4. 动态添加div后的三大“灵魂拷问”
4.1 事件绑定为什么失效
这是日常踩坑排行榜第一名。现象是:页面加载后用append添加的div,点击没反应。原因很简单:直接绑定在已有元素上的事件,不会自动作用于未来创建的元素。
// 错误示范:动态div没有点击事件 $('.item').on('click', function() { console.log('点击了'); }); $('#box').append('<div class="item">动态div</div>');上面代码里,$('.item')在事件绑定时页面里还没有任何.item元素,或者只绑定了当时存在的元素,后面append进来的div当然没有事件。解决办法就是事件委托:
// 正确做法:把监听挂到容器上,事件通过冒泡触发 $('#box').on('click', '.item', function() { console.log('点击了', this); });原生JS同样可以委托:
document.getElementById('box').addEventListener('click', function(e) { const target = e.target.closest('.item'); if (target) { console.log('点击了', target); } });事件委托的原理是:子元素的事件会冒泡到父容器,父容器统一监听,再通过e.target判断点击的是谁。这样一来,不管以后往容器里加多少个div,事件都能正常工作,因为它监听的是容器本身,而不是某个特定子元素。
4.2 动态添加的div为什么没有样式
这个问题通常是两种原因。一种是因为样式写在了某个局部作用域里,动态加载的DOM不在这个范围内,比如Vue的scoped样式只对当前组件模板中的静态部分生效;另一种是你加了class但CSS规则优先级不够。
纯jQuery或者原生场景,最常见的是拼接字符串时把class拼错了:
<div class='item'">内容</div>这种引号错位会导致整个class解析失败。排查办法很简单:打开控制台,用$0选中刚插入的元素,看一下它的className值,再检查CSS选择器写的是不是暗合。如果是项目用了CSS Modules或者BEM,动态插入的div里写的类名和生成后的哈希类名对不上,样式自然就丢了。可以先把元素的outerHTML复制出来检查,问题多半一眼就能看出来。
4.3 动态添加后怎么获取元素
添加完div后,很多人下意识去getElementById,结果拿到的是null。原因往往是:
- 插入前后DOM还没更新到渲染层(很少见,但某些老浏览器里有);
- 元素的id里带了引号没转义;
- 拼字符串时id写成了动态变量但取值不对。
获取动态元素最稳的方法:插入后立刻用容器查询,或者干脆在创建时就把变量存起来引用。
// 推荐方式:直接保存引用 const div = document.createElement('div'); div.id = 'item-' + index; box.appendChild(div); // 之后直接用div变量就行,不用再查找 // 如果必须要查询 const findDiv = document.getElementById('item-' + index); // 慎用:id里有特殊字符时可能失效推荐用querySelector搭配属性选择器:
const target = box.querySelector('[data-id="' + index + '"]');把>const box = document.getElementById('box'); // 渲染列表 function renderList(data) { let html = ''; data.forEach(item => { html += `<div class="item">function escapeHtml(str) { return String(str) .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, '''); }
这个函数虽然只覆盖了最基础的字符,但已经能挡住大部分意外的HTML注入问题。放在工具函数里,拼接时给变量包一层:
box.insertAdjacentHTML('beforeend', `<div>${escapeHtml(userInput)}</div>`);有人会觉得麻烦,但踩过一次XSS或者页面被改乱的坑就不嫌麻烦了。
写到最后我想说,append这个操作本身只是一个入口,真正决定代码质量的往往是你围绕这个入口做的选择:用什么方式创建节点、怎么批量插入、事件怎么绑定、内容怎么处理。这些细节组合起来,才是日常业务代码和高品质代码之间真正的差距。把自己踩过的坑、养成的好习惯沉淀下来,比记多少API都管用。