跳到主要内容

DOM 节点操作全指南:创建、查找、添加、移动、复制与移除

前端里很多“看起来是界面更新”的事情,本质上都是 DOM 节点操作

  • 创建一个新按钮
  • 把一段内容插到列表顶部
  • 把节点从 A 容器移动到 B 容器
  • 复制一份卡片模板
  • 删除一个弹窗
  • 找到某个元素的父节点、兄弟节点或最近的祖先节点

如果你把这些 API 的行为吃透,日常开发里大部分原生 DOM 操作都能应对。

一句话先记住: DOM 是一棵树,节点操作就是在这棵树上做“增、删、改位、复制、查找”。


1. 先建立模型:什么是“节点”?

DOM 不是只包含元素节点。常见节点类型有:

类型说明典型 API
Element元素节点,如 divlibuttonquerySelector()children
Text文本节点,如标签里的文字textContentcreateTextNode()
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,可能还包含换行产生的文本节点
记忆技巧

平时做业务开发,大多数时候你真正想操作的是 元素节点,所以优先用 childrenfirstElementChildnextElementSibling 这一组 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 childrenchildNodes 搞混

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:childrenchildNodes 有什么区别?

答: 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 个核心原则:

  1. 同一个节点再次插入 = 移动,不是复制
  2. 复制模板优先 cloneNode(true),记得处理重复 id
  3. 平时优先操作元素节点,不要把 childrenchildNodes 混了

那你对 DOM 基础操作就已经掌握得很扎实了。

你可以把这篇和同目录的 Selection API 与 getClientRects()requestAnimationFrame 搭配着看,会更容易把“节点操作 + 交互 + 渲染时机”串起来。