在了解了 DOM 节点树的结构与节点类型(13.1 节)之后,最核心的开发任务就是动态地操作这些节点。无论是响应用户点击添加一个列表项、删除一条评论,还是将一段模板克隆后插入页面,都需要掌握 DOM 节点的基本操作。本节按照“查找→创建→插入→删除→替换→克隆”的顺序,列出最常用、最实用的方法。
13.2.1 查找节点
查找是一切 DOM 操作的起点。现代浏览器提供了强大的选择器 API,可以根据需要灵活选择。
1. 通过 CSS 选择器查找
// 返回第一个匹配的元素
const header = document.querySelector('.header');
const activeItem = document.querySelector('ul li.active');
// 返回所有匹配的元素(NodeList)
const allItems = document.querySelectorAll('ul li');
// 对 NodeList 可以使用 forEach 或转为数组
allItems.forEach(item => console.log(item.textContent));
注意:querySelectorAll 返回的是静态 NodeList,不会随 DOM 变化而自动更新。如果之后 DOM 发生变化,需要重新查询。
2. 通过 ID / 标签 / 类名查找(传统方法)
// ID(返回单个元素)
const main = document.getElementById('main');
// 标签名(返回动态 HTMLCollection)
const divs = document.getElementsByTagName('div');
// 类名(返回动态 HTMLCollection,支持多个类名)
const cards = document.getElementsByClassName('card');
// name 属性(常用于表单)
const radios = document.getElementsByName('gender');
注意:getElementsByTagName 和 getElementsByClassName 返回的是动态 HTMLCollection,一旦页面元素发生变化,集合会自动反映更新。这在某些场景下很方便,但也容易在遍历时踩坑(比如一边遍历一边删除节点会导致索引错乱)。
3. 关系型查找(从已知节点出发)
const child = document.getElementById('child');
const parent = child.parentNode; // 父节点
const siblings = child.parentNode.children; // 所有子元素(HTMLCollection)
const next = child.nextElementSibling; // 下一个兄弟元素
const prev = child.previousElementSibling; // 上一个兄弟元素
区分:nextSibling / previousSibling 可能返回文本节点(换行符等),而 nextElementSibling / previousElementSibling 只返回元素节点,一般优先使用带 Element 的版本。
13.2.2 创建节点
动态创建节点有三种典型场景:创建普通元素、创建文本、利用 HTML 片段快速生成。
// 创建一个元素节点
const div = document.createElement('div');
div.className = 'box';
div.setAttribute('data-id', '1');
// 创建文本节点(通常可以直接使用 textContent)
const text = document.createTextNode('Hello');
// 更常见的做法:直接用 textContent 或 innerHTML
div.textContent = '这是一段文字';
// 通过 HTML 字符串快速生成(注意安全)
const template = document.createElement('div');
template.innerHTML = '<span class="tag">热门</span><p>内容</p>';
const fragment = template.children; // 取出子元素
innerHTML 与 createElement 的选择:
- 简单结构:直接用
createElement更安全、性能稍好。 - 复杂结构或模板:
innerHTML更简洁,但需要注意防止 XSS 攻击,不要插入用户输入的内容。 - 大量节点:可以考虑用
DocumentFragment(文档片段),在内存中构建好整个 DOM 树后再一次性插入,减少回流。
13.2.3 插入节点
将创建好的节点放入文档中,方法很多,各有适用场景。
const parent = document.getElementById('container');
const child = document.createElement('p');
child.textContent = '新插入的段落';
// 1. 末尾追加(最常用)
parent.appendChild(child);
// 2. 插入到指定子节点之前
const reference = document.getElementById('reference');
parent.insertBefore(child, reference);
// 3. 现代方法(更灵活)
parent.append(child, '纯文本也可以');
parent.prepend(child); // 插入为第一个子节点
reference.before(child); // 插入在 reference 之前(同级)
reference.after(child); // 插入在 reference 之后(同级)
方法对比:
| 方法 | 说明 | 示例 |
|------|------|------|
| appendChild(node) | 将 node 作为最后一个子节点插入 | parent.appendChild(child) |
| insertBefore(new, ref) | 将 new 插入到 ref 之前(必须是同一个父元素) | parent.insertBefore(child, ref) |
| append(...nodes) | 末尾追加,支持多个节点和字符串 | parent.append(a, b, 'text') |
| prepend(...nodes) | 开头插入 | parent.prepend(child) |
| before(...nodes) | 在同级元素之前插入 | ref.before(child) |
| after(...nodes) | 在同级元素之后插入 | ref.after(child) |
注意:如果插入的节点已经是文档中的现有节点,appendChild 等操作会先将其从原位置移除,再移动到新位置。这是节点移动的常用技巧,无需手动删除。
13.2.4 删除节点
删除节点必须通过其父元素调用 removeChild,或者直接使用元素的自删除方法。
// 传统方式:通过父元素删除
const child = document.getElementById('toRemove');
child.parentNode.removeChild(child);
// 现代方式:直接调用 remove(无需去找父元素)
child.remove();
安全删除:确保节点确实存在。如果 child.parentNode 为 null(节点已脱离文档),会抛出错误。可以添加判断:
if (child && child.parentNode) {
child.parentNode.removeChild(child);
}
// 或
child?.remove();
批量删除:如果循环遍历 HTMLCollection 时同时删除,记得反向遍历或转换为数组再操作,否则索引会错乱。
// 错误:正向遍历可能漏删
// 正确:转为数组后删除
Array.from(document.querySelectorAll('.delete-me')).forEach(el => el.remove());
13.2.5 替换节点
将一个节点替换为另一个节点,同样涉及父元素。
// 传统方法
const oldNode = document.getElementById('old');
const newNode = document.createElement('div');
newNode.textContent = '我是新内容';
oldNode.parentNode.replaceChild(newNode, oldNode);
// 现代方法
oldNode.replaceWith(newNode);
// replaceWith 也支持多个节点和文本
oldNode.replaceWith(newNode, '额外文本');
应用场景:列表项的局部刷新、错误提示的替换、组件内容的完全更新等。
13.2.6 克隆节点
有时候我们不需要从头创建节点,而是复制一个现有节点。克隆分浅拷贝和深拷贝。
const original = document.getElementById('template');
// 浅克隆:只克隆元素本身,不复制子节点
const shallow = original.cloneNode(false);
// 深克隆:克隆元素及其所有后代
const deep = original.cloneNode(true);
// 将克隆后的节点插入到某处
document.body.appendChild(deep);
注意:
- 克隆只会复制 DOM 结构和行内样式,不会复制通过 CSS 文件添加的样式(样式仍会应用于克隆后的节点,因为它还是被选择器匹配)。
- 事件监听器(通过
addEventListener绑定的)不会被克隆。因为事件监听器不是 DOM 属性,附加在 JavaScript 对象上,不在 DOM 节点结构中。如果需要转移事件,必须重新绑定。 - 如果元素上有
id,克隆后会出现重复 ID,需手动修改,否则页面会有非法 HTML 且getElementById可能只返回第一个。
13.2.7 实战要点与性能意识
- 减少 DOM 操作次数:每一次插入、删除都可能引发回流(reflow)/重绘(repaint),频繁操作性能代价高。优先在内存中构建完整节点树(如使用
DocumentFragment或temp.innerHTML构建),最后一次性插入。
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const li = document.createElement('li');
li.textContent = `Item ${i}`;
fragment.appendChild(li);
}
document.getElementById('list').appendChild(fragment);
- 使用合适的查找方法:如果只需要 ID 查找,
getElementById比querySelector更快。但现代浏览器差异很小,可读性通常更重要。
- 注意动态集合的陷阱:遍历
HTMLCollection时若同时增删节点,务必倒序遍历或先转数组。
- 避免 XSS 安全风险:直接从用户输入构造 HTML 插入(如
innerHTML = userInput)极其危险。优先使用textContent设置文字,或进行消毒处理(如使用 DOMPurify 库)。
- 节点引用与内存泄漏:被删除的节点如果还被 JavaScript 变量引用,不会立即被垃圾回收,注意手动解除不必要的引用。
掌握这些基本的节点操作方法,你就拥有了用 JavaScript 自由操控页面结构的能力——表单动态生成、列表分页渲染、弹窗创建销毁、拖拽重排,这些常见功能都建立在这些原子操作之上。