DOM 节点操作全指南:创建、查找、添加、移动、复制与移除
前端里很多“看起来是界面更新”的事情,本质上都是 DOM 节点操作:
- 创建一个新按钮
- 把一段内容插到列表顶部
- 把节点从 A 容器移动到 B 容器
- 复制一份卡片模板
- 删除一个弹窗
- 找到某个元素的父节点、兄弟节点或最近的祖先节点
如果你把这些 API 的行为吃透,日常开发里大部分原生 DOM 操作都能应对。
一句话先记住: DOM 是一棵树,节点操作就是在这棵树上做“增、删、改位、复制、查找”。
1. 先建立模型:什么是“节点”?
DOM 不是只包含元素节点。常见节点类型有:
| 类型 | 说明 | 典型 API |
|---|---|---|
Element | 元素节点,如 div、li、button | querySelector()、children |
Text | 文本节点,如标签里的文字 | textContent、createTextNode() |
Document | 整个文档对象 | document |
DocumentFragment | 文档片段,适合批量插入 | createDocumentFragment() |
最容易踩坑的一点:
children:只包含元素节点childNodes:包含元素、文本、注释等所有节点
例如:
<ul id="list">
<li>一</li>
<li>二</li>
</ul>
const list = document.querySelector('#list');
console.log(list.children); // HTMLCollection,只看 li 元素
console.log(list.childNodes); // NodeList,可能还包含换行产生的文本节点
平时做业务开发,大多数时候你真正想操作的是 元素节点,所以优先用 children、firstElementChild、nextElementSibling 这一组 API。
2. 创建节点:先有“料”,再谈插入
2.1 创建元素节点:document.createElement()
这是最常用的创建方式。
const button = document.createElement('button');
button.textContent = '提交';
button.className = 'primary-btn';
button.type = 'button';
你也可以继续补充属性:
button.id = 'submit-btn';
button.dataset.action = 'submit';
button.setAttribute('aria-label', '提交表单');
2.2 创建文本节点:document.createTextNode()
虽然大多数时候直接写 textContent 就够了,但它仍然值得知道。
const textNode = document.createTextNode('这是按钮文字');
button.appendChild(textNode);
通常这两种写法等价,但更推荐:
button.textContent = '这是按钮文字';
因为更直接,也不容易写出多余代码。
2.3 创建文档片段:document.createDocumentFragment()
当你要批量插入多个节点时,DocumentFragment 很有用。
const fragment = document.createDocumentFragment();
for (let index = 1; index <= 3; index += 1) {
const li = document.createElement('li');
li.textContent = `第 ${index} 项`;
fragment.appendChild(li);
}
document.querySelector('#list').appendChild(fragment);
特点:
- 片段本身不是最终页面中的可见元素
- 把
fragment插入 DOM 时,里面的子节点会整体搬进去 - 适合做列表渲染、批量拼装节点
2.4 字符串创建:innerHTML 能不能用?
能用,但它和“创建节点 API”不是一回事。
document.querySelector('#list').innerHTML = '<li>苹果</li><li>香蕉</li>';
优点:
- 写起来快
- 一次性插入结构方便
缺点:
- 会触发字符串解析
- 容易引入 XSS 风险(尤其是拼接用户输入时)
- 容易把原有子节点整个覆盖掉
经验法则:
- 固定模板、小段静态结构:可以考虑
innerHTML - 需要精细控制、逐步构建、避免安全问题:优先原生节点 API
3. 查找节点:先找到,再操作
3.1 最常用:querySelector() 与 querySelectorAll()
const card = document.querySelector('.card');
const items = document.querySelectorAll('.todo-item');
区别:
| API | 返回值 | 特点 |
|---|---|---|
querySelector() | 第一个匹配元素,没有则 null | 最常用 |
querySelectorAll() | 静态 NodeList | 可遍历,不会随 DOM 自动更新 |
items.forEach((item) => {
console.log(item.textContent);
});
3.2 更快更直接的老牌 API
const app = document.getElementById('app');
const inputs = document.getElementsByClassName('input');
const paragraphs = document.getElementsByTagName('p');
其中要注意:
getElementById()返回单个元素getElementsByClassName()/getElementsByTagName()返回的是 实时集合(live collection)
也就是说,DOM 改了,集合内容会跟着变。
const listItems = document.getElementsByTagName('li');
console.log(listItems.length);
const li = document.createElement('li');
li.textContent = '新项';
document.querySelector('#list').appendChild(li);
console.log(listItems.length); // 会变
3.3 向上找祖先:closest()
做事件委托时非常常用。
document.querySelector('#list').addEventListener('click', (event) => {
const item = event.target.closest('.todo-item');
if (!item) return;
console.log('点击的是:', item.dataset.id);
});
这段代码的意思是:
- 用户可能点到了按钮、图标、文字
- 但我真正想找的是最近的
.todo-item
3.4 判断是否匹配:matches()
if (element.matches('.active')) {
console.log('当前元素处于激活态');
}
3.5 关系查找:父、子、兄弟节点
常用速查:
| 目标 | 推荐 API |
|---|---|
| 父元素 | parentElement |
| 所有子元素 | children |
| 第一个子元素 | firstElementChild |
| 最后一个子元素 | lastElementChild |
| 下一个兄弟元素 | nextElementSibling |
| 上一个兄弟元素 | previousElementSibling |
| 包含文本/注释在内的所有子节点 | childNodes |
const current = document.querySelector('.current');
console.log(current.parentElement);
console.log(current.nextElementSibling);
console.log(current.previousElementSibling);
4. 添加节点:把节点放到树上
4.1 现代常用:append() 与 prepend()
const list = document.querySelector('#list');
const li = document.createElement('li');
li.textContent = '最后一项';
list.append(li);
插到最前面:
list.prepend(li);
append() / prepend() 的优点:
- 可以一次传多个参数
- 可以直接传字符串
list.append('尾部文本', document.createElement('li'));
4.2 经典 API:appendChild()
list.appendChild(li);
它和 append() 的主要区别:
| API | 能传字符串吗 | 能一次传多个参数吗 | 返回值 |
|---|---|---|---|
append() | 能 | 能 | undefined |
appendChild() | 不能 | 不能 | 被插入的节点 |
如果你只插入一个节点,appendChild() 仍然完全可用。
4.3 插到指定位置:before()、after()、insertBefore()
已知一个参考节点时,你可以把新节点插到它前后。
const target = document.querySelector('.target');
const badge = document.createElement('span');
badge.textContent = 'New';
target.before(badge);
插到后面:
target.after(badge);
老牌写法:
const parent = target.parentNode;
parent.insertBefore(badge, target);
如果你想“插到某个节点后面”,用 insertBefore() 需要这样写:
parent.insertBefore(badge, target.nextSibling);
5. 移动节点:DOM 里“同一个节点只能在一个位置”
这是 DOM 操作里最重要的认知之一。
同一个节点不可能同时出现在两个位置。
如果你把一个已经存在的节点再次 appendChild() 到别处,它不是“复制”过去,而是移动过去。
const item = document.querySelector('.todo-item');
const doneList = document.querySelector('#done-list');
doneList.appendChild(item);
执行结果:
item会从原来的列表中消失- 然后出现在
#done-list中 - 不会产生两份
5.1 为什么很多“重新排序”其实只是移动?
比如把列表最后一项挪到最前面:
const list = document.querySelector('#list');
const last = list.lastElementChild;
if (last) {
list.prepend(last);
}
这里没有创建新节点,也没有删除旧节点,只是把原节点移动了位置。
5.2 用 insertBefore() 做精确移动
const list = document.querySelector('#list');
const source = document.querySelector('[data-id="3"]');
const target = document.querySelector('[data-id="1"]');
if (source && target) {
list.insertBefore(source, target);
}
这会把 source 移动到 target 前面。
appendChild()、append()、prepend()、insertBefore() 遇到“已有节点”时,默认行为都是 移动,不是复制。
6. 复制节点:cloneNode() 才是真正的“拷贝”
6.1 浅拷贝 vs 深拷贝
const card = document.querySelector('.card');
const copy1 = card.cloneNode();
const copy2 = card.cloneNode(true);
区别:
cloneNode(false):只复制当前元素本身,不复制子节点cloneNode(true):深拷贝,连后代节点一起复制
6.2 典型场景:复制卡片模板
const template = document.querySelector('.card-template');
const container = document.querySelector('.card-list');
const newCard = template.cloneNode(true);
newCard.classList.remove('card-template');
newCard.querySelector('.title').textContent = '新卡片';
container.appendChild(newCard);
6.3 cloneNode() 不会复制什么?
这是面试和实战都高频的点。
通常要记住:
- 会复制结构、属性、内联内容
- 不会复制通过
addEventListener()绑定的事件监听器 - 复制后如果包含相同
id,会导致页面里出现重复id
const button = document.querySelector('#save-btn');
button.addEventListener('click', () => {
console.log('保存');
});
const newButton = button.cloneNode(true);
console.log(newButton.id); // 还是 save-btn,可能冲突
所以复制后常见动作是:
newButton.id = 'save-btn-copy';
6.4 跨文档复制:importNode()(进阶)
如果节点来自另一个 document(比如 iframe 或其他文档上下文),常见做法是:
const importedNode = document.importNode(otherNode, true);
大部分普通业务开发很少用到,但知道它的用途就够了。
7. 移除节点:删除要干净
7.1 最推荐:remove()
const dialog = document.querySelector('.dialog');
dialog?.remove();
优点:
- 写法最直观
- 不需要先拿到父节点
7.2 经典写法:removeChild()
const parent = document.querySelector('#list');
const child = document.querySelector('.todo-item');
if (parent && child) {
parent.removeChild(child);
}
它会返回被删除的节点:
const removedNode = parent.removeChild(child);
console.log(removedNode);
7.3 清空一个容器
方式一:
const container = document.querySelector('#list');
container.innerHTML = '';
方式二:
const container = document.querySelector('#list');
container.replaceChildren();
方式三(老写法):
const container = document.querySelector('#list');
while (container.firstChild) {
container.removeChild(container.firstChild);
}
如果你只是想快速清空,优先考虑:
container.replaceChildren();
它的语义最明确:把现有子节点替换为空。
8. 一次性串起来:完整示例
下面这个例子把“创建、查找、添加、移动、复制、移除”串成一套:
<ul id="todo-list">
<li class="todo-item" data-id="1">学习 DOM</li>
<li class="todo-item" data-id="2">写练习</li>
</ul>
<ul id="done-list"></ul>
const todoList = document.querySelector('#todo-list');
const doneList = document.querySelector('#done-list');
// 1)创建
const newItem = document.createElement('li');
newItem.className = 'todo-item';
newItem.dataset.id = '3';
newItem.textContent = '复习节点操作';
// 2)添加
if (todoList) {
todoList.appendChild(newItem);
}
// 3)查找
const firstTodo = document.querySelector('.todo-item');
console.log(firstTodo?.textContent);
// 4)移动
const itemToMove = document.querySelector('[data-id="1"]');
if (itemToMove && doneList) {
doneList.appendChild(itemToMove);
}
// 5)复制
const itemToCopy = document.querySelector('[data-id="2"]');
if (itemToCopy && doneList) {
const clonedItem = itemToCopy.cloneNode(true);
clonedItem.dataset.id = '2-copy';
doneList.appendChild(clonedItem);
}
// 6)移除
const itemToRemove = document.querySelector('[data-id="3"]');
itemToRemove?.remove();
执行后你会看到:
- 新任务先被创建并加到待办列表
data-id="1"的任务被移动到已完成列表data-id="2"的任务被复制一份到已完成列表data-id="3"的任务随后被删除
9. 高频坑点总结
9.1 children 和 childNodes 搞混
console.log(element.children); // 只看元素
console.log(element.childNodes); // 还包含文本节点
如果页面结构里有换行缩进,你会经常看到额外的文本节点。
9.2 以为追加已有节点会复制
不会。追加已有节点是移动。
9.3 cloneNode(true) 后忘了处理重复 id
这会影响:
getElementById()的结果- label 与表单控件的关联
- CSS/JS 选择器命中
9.4 querySelectorAll() 不是实时集合
const items = document.querySelectorAll('li');
console.log(items.length);
list.appendChild(document.createElement('li'));
console.log(items.length); // 不会变
9.5 批量插入时反复操作 DOM
如果节点很多,可以先用 DocumentFragment 组装,再一次性插入。
9.6 删除节点后,变量还在
const box = document.querySelector('.box');
box.remove();
console.log(box); // 变量还引用着这个节点对象
只是它已经不在文档树里了,不代表 JS 变量立刻消失。
10. 实战建议:写业务时怎么选 API?
10.1 我只想找元素
优先:
querySelector()querySelectorAll()closest()
10.2 我只想追加一个节点
优先:
append()- 或
appendChild()
10.3 我想移动已有节点
优先:
appendChild()prepend()insertBefore()
10.4 我想复制模板
优先:
cloneNode(true)
10.5 我想删除节点
优先:
remove()- 批量清空用
replaceChildren()
11. 面试高频问答
Q1:append() 和 appendChild() 有什么区别?
答: append() 更现代,能传多个参数,也能直接传字符串;appendChild() 只能传一个节点,但会返回这个节点。日常开发两者都常用。
Q2:为什么 appendChild() 一个已有节点时,原位置的节点没了?
答: 因为 DOM 中同一个节点实例只能存在于一个位置。再次插入时,浏览器执行的是“移动”而不是“复制”。
Q3:cloneNode(true) 和 cloneNode(false) 的区别是什么?
答: true 表示深拷贝,会把后代节点一起复制;false 表示浅拷贝,只复制当前节点本身。
Q4:cloneNode(true) 会复制事件吗?
答: 一般不会复制通过 addEventListener() 注册的事件监听器,所以复制后如果需要交互,通常要重新绑定事件,或者用事件委托。
Q5:querySelectorAll() 返回的是数组吗?
答: 不是数组,而是 NodeList。它可以 forEach(),但不等于真正的数组;而且它是静态集合。
Q6:children 和 childNodes 有什么区别?
答: children 只包含元素节点;childNodes 包含元素、文本、注释等所有节点。排查“为什么多出来一个节点”时,这个区别非常关键。
Q7:为什么批量插入时常说用 DocumentFragment?
答: 因为它适合先在内存里把节点组织好,再一次性插入到页面,代码语义清晰,也更适合批量拼装结构。
Q8:删除一个节点后,事件监听器和变量会立刻消失吗?
答: 不一定。节点从 DOM 树中移除了,但如果 JS 里还有变量引用它,或者相关闭包仍然存在,它就未必能立刻被回收。
12. 最后总结
把 DOM 节点操作真正吃透,只要抓住 6 个动作:
- 创建:
createElement()、createTextNode()、createDocumentFragment() - 查找:
querySelector()、querySelectorAll()、closest() - 添加:
append()、prepend()、before()、after()、appendChild() - 移动:把“已有节点”重新插入到新位置
- 复制:
cloneNode(true) - 移除:
remove()、removeChild()、replaceChildren()
如果你还能顺手记住这 3 个核心原则:
- 同一个节点再次插入 = 移动,不是复制
- 复制模板优先
cloneNode(true),记得处理重复id - 平时优先操作元素节点,不要把
children和childNodes混了
那你对 DOM 基础操作就已经掌握得很扎实了。
你可以把这篇和同目录的 Selection API 与 getClientRects()、requestAnimationFrame 搭配着看,会更容易把“节点操作 + 交互 + 渲染时机”串起来。