DOM常见的操作有哪些?
问题解析
DOM(Document Object Model)是 HTML 和 XML 文档的编程接口,它提供了对文档的结构化表述,并定义了一种方式可以使程序对该结构进行访问,从而改变文档的结构、样式和内容。掌握 DOM 操作是前端开发的基础技能。
核心概念
DOM 树结构
任何 HTML 文档都可以用 DOM 表示为一个由节点构成的层级结构:
<html>
<head>
<title>Page</title>
</head>
<body>
<p>Hello World!</p>
</body>
</html>
节点类型:
- 元素节点:HTML 标签(如
div、p) - 文本节点:标签内的文本内容
- 属性节点:元素的属性(如
title、class) - 注释节点:HTML 注释
详细解答
一、创建节点
1. createElement - 创建元素节点
// 创建一个新的 div 元素
const divEl = document.createElement("div");
divEl.className = "container";
divEl.id = "main";
divEl.textContent = "这是一个新创建的 div";
2. createTextNode - 创建文本节点
const textEl = document.createTextNode("这是文本内容");
const pEl = document.createElement("p");
pEl.appendChild(textEl);
3. createDocumentFragment - 创建文档片段
// 文档片段是一种轻量级的文档,用于存储临时节点
const fragment = document.createDocumentFragment();
// 批量创建节点,减少 DOM 重绘重排
for (let i = 0; i < 100; i++) {
const li = document.createElement("li");
li.textContent = `Item ${i}`;
fragment.appendChild(li);
}
// 一次性添加到 DOM 中
document.getElementById("list").appendChild(fragment);
4. createAttribute - 创建属性节点
const attr = document.createAttribute("data-custom");
attr.value = "custom-value";
const div = document.createElement("div");
div.setAttributeNode(attr);
console.log(div.getAttribute("data-custom")); // "custom-value"
二、获取节点
1. querySelector - 获取单个元素
// 通过类名获取
document.querySelector('.element');
// 通过 ID 获取
document.querySelector('#element');
// 通过标签名获取
document.querySelector('div');
// 通过属性获取
document.querySelector('[name="username"]');
// 复杂选择器
document.querySelector('div + p > span');
// 如果没有匹配元素,返回 null
2. querySelectorAll - 获取所有匹配元素
// 返回 NodeList(静态集合)
const elements = document.querySelectorAll(".item");
// 支持遍历
elements.forEach(el => {
console.log(el.textContent);
});
// 转换为数组
const arr = Array.from(elements);
// 或
const arr2 = [...elements];
3. 其他获取方法
// 通过 ID 获取(效率最高)
document.getElementById('id');
// 通过类名获取(返回 HTMLCollection,动态集合)
document.getElementsByClassName('class');
// 通过标签名获取
document.getElementsByTagName('div');
// 通过 name 属性获取
document.getElementsByName('username');
// 获取 HTML 和 BODY
document.documentElement; // <html>
document.body; // <body>
document.head; // <head>
4. 节点关系属性
const element = document.getElementById('demo');
// 父子关系
element.parentNode; // 父节点
element.parentElement; // 父元素节点
element.childNodes; // 所有子节点(包括文本节点)
element.children; // 子元素节点
element.firstChild; // 第一个子节点
element.firstElementChild; // 第一个子元素节点
element.lastChild; // 最后一个子节点
element.lastElementChild; // 最后一个子元素节点
// 兄弟关系
element.nextSibling; // 下一个兄弟节点
element.nextElementSibling; // 下一个兄弟元素节点
element.previousSibling; // 上一个兄弟节点
element.previousElementSibling; // 上一个兄弟元素节点
三、更新节点
1. innerHTML - 设置/获取 HTML 内容
const p = document.getElementById('p');
// 设置文本
p.innerHTML = 'ABC';
// 设置 HTML(会解析标签)
p.innerHTML = 'ABC <span style="color:red">RED</span> XYZ';
// 获取 HTML
console.log(p.innerHTML);
// ⚠️ 注意:使用 innerHTML 有 XSS 风险,不要插入不可信内容
2. innerText / textContent - 设置/获取文本内容
const p = document.getElementById('p');
// innerText:不返回隐藏元素的文本
p.innerText = '<script>alert("Hi")<\/script>'; // 会被转义为纯文本
// textContent:返回所有文本,包括隐藏元素
p.textContent = '纯文本内容';
// 区别:
// 1. innerText 会触发重排,性能较差
// 2. textContent 不会触发重排,性能更好
// 3. innerText 会考虑 CSS 样式,textContent 不会
3. style - 修改样式
const p = document.getElementById('p');
// 直接设置样式(内联样式)
p.style.color = '#ff0000';
p.style.fontSize = '20px'; // 驼峰命名
p.style.backgroundColor = 'yellow';
p.style.paddingTop = '2em';
// 设置多个样式
p.style.cssText = 'color: red; font-size: 20px; padding: 10px;';
// 获取计算样式(只读)
const styles = window.getComputedStyle(p);
console.log(styles.color);
4. setAttribute / getAttribute - 操作属性
const div = document.getElementById('id');
// 设置属性
div.setAttribute('class', 'new-class');
div.setAttribute('data-id', '123');
div.setAttribute('disabled', '');
// 获取属性
const className = div.getAttribute('class');
// 删除属性
div.removeAttribute('data-id');
// 检查属性
const hasClass = div.hasAttribute('class');
5. classList - 操作类名
const element = document.getElementById('demo');
// 添加类名
element.classList.add('active');
element.classList.add('class1', 'class2');
// 删除类名
element.classList.remove('active');
// 切换类名(有则删除,无则添加)
element.classList.toggle('active');
element.classList.toggle('active', true); // 强制添加
element.classList.toggle('active', false); // 强制删除
// 检查是否包含类名
const isActive = element.classList.contains('active');
// 替换类名
element.classList.replace('old-class', 'new-class');
四、添加节点
1. appendChild - 在末尾添加子节点
const parent = document.getElementById('list');
const newItem = document.createElement('li');
newItem.textContent = 'New Item';
// 添加到父节点末尾
parent.appendChild(newItem);
// ⚠️ 如果节点已存在于文档中,会先移除再添加(移动操作)
const existing = document.getElementById('existing');
parent.appendChild(existing); // 从原位置移动到新位置
2. insertBefore - 在指定位置插入
const parent = document.getElementById('list');
const newItem = document.createElement('li');
newItem.textContent = '插入的项';
const referenceNode = document.getElementById('ref');
// 在 referenceNode 之前插入
parent.insertBefore(newItem, referenceNode);
// 如果 referenceNode 为 null,则在末尾添加
parent.insertBefore(newItem, null); // 等同于 appendChild
3. insertAdjacentHTML / insertAdjacentElement
const element = document.getElementById('target');
// 插入 HTML 字符串
// beforebegin: 元素之前
// afterbegin: 元素内部,第一个子节点之前
// beforeend: 元素内部,最后一个子节点之后
// afterend: 元素之后
element.insertAdjacentHTML('beforeend', '<p>新内容</p>');
// 插入元素
const newEl = document.createElement('span');
element.insertAdjacentElement('afterbegin', newEl);
// 插入文本
element.insertAdjacentText('beforeend', '纯文本');
4. ES6 新增方法
const parent = document.getElementById('parent');
const child = document.createElement('div');
// append - 可以添加多个节点或字符串
parent.append(child, '文本内容');
// prepend - 在开头添加
parent.prepend(child);
// before - 在元素之前添加
child.before(sibling);
// after - 在元素之后添加
child.after(sibling);
// replaceWith - 替换元素
child.replaceWith(newChild);
五、删除节点
1. removeChild - 移除子节点
// 获取要删除的节点
const self = document.getElementById('to-be-removed');
// 获取父节点
const parent = self.parentNode;
// 删除节点
const removed = parent.removeChild(self);
// removed === self,节点仍在内存中,可以重新添加
2. remove - 直接删除自身(ES6)
const element = document.getElementById('to-remove');
element.remove(); // 更简洁的方式
3. innerHTML = '' - 清空子节点
const container = document.getElementById('container');
// 清空所有子节点
container.innerHTML = '';
// 或使用 while 循环(更高效)
while (container.firstChild) {
container.removeChild(container.firstChild);
}
六、克隆节点
const original = document.getElementById('original');
// 浅克隆:只克隆节点本身,不克隆子节点
const shallowClone = original.cloneNode(false);
// 深克隆:克隆节点及其所有子节点
const deepClone = original.cloneNode(true);
// 添加到文档中
document.body.appendChild(deepClone);
// ⚠️ cloneNode 不会克隆事件监听器
// ⚠️ id 属性也会被克隆,需要手动修改避免重复
深入理解
DOM 操作性能优化
// ❌ 不好的做法:频繁操作 DOM
for (let i = 0; i < 100; i++) {
document.body.innerHTML += `<div>${i}</div>`; // 每次都会重绘
}
// ✅ 好的做法:使用文档片段
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
const div = document.createElement('div');
div.textContent = i;
fragment.appendChild(div);
}
document.body.appendChild(fragment);
// ✅ 或使用 innerHTML 一次性设置
let html = '';
for (let i = 0; i < 100; i++) {
html += `<div>${i}</div>`;
}
document.body.innerHTML = html;
重排(Reflow)与重绘(Repaint)
// 重排:当元素尺寸、位置发生变化时触发
// 以下操作会触发重排:
// - 添加/删除可见 DOM 元素
// - 改变元素位置、尺寸
// - 改变窗口大小
// - 读取某些属性(offsetTop, scrollTop, clientWidth 等)
// ❌ 强制同步布局(避免)
const el = document.getElementById('el');
el.style.width = '100px';
console.log(el.offsetHeight); // 强制浏览器立即计算布局
el.style.height = '100px';
// ✅ 批量读写,使用 requestAnimationFrame
requestAnimationFrame(() => {
el.style.width = '100px';
el.style.height = '100px';
});
最佳实践
-
缓存 DOM 查询结果
// ❌ 不要重复查询 for (let i = 0; i < 100; i++) { document.getElementById('item').style.left = i + 'px'; } // ✅ 缓存引用 const item = document.getElementById('item'); for (let i = 0; i < 100; i++) { item.style.left = i + 'px'; } -
使用事件委托减少监听器数量
// ❌ 为每个子元素添加监听器 document.querySelectorAll('li').forEach(li => { li.addEventListener('click', handler); }); // ✅ 使用事件委托 document.getElementById('list').addEventListener('click', e => { if (e.target.tagName === 'LI') { handler(e); } }); -
批量操作 DOM 时使用文档片段
-
使用 classList 代替直接操作 className
-
避免在循环中使用 innerHTML +=
面试要点
-
DOM 操作的分类?
- 创建节点:createElement、createTextNode、createDocumentFragment
- 查询节点:querySelector、querySelectorAll、getElementById 等
- 更新节点:innerHTML、textContent、style、setAttribute
- 添加节点:appendChild、insertBefore、append、prepend
- 删除节点:removeChild、remove
-
querySelector 和 getElementById 的区别?
- querySelector 返回第一个匹配的元素,支持 CSS 选择器
- getElementById 只通过 ID 查找,效率更高
- querySelectorAll 返回静态 NodeList,getElementsByTagName/ClassName 返回动态 HTMLCollection
-
innerHTML 和 textContent 的区别?
- innerHTML 解析 HTML 标签,有 XSS 风险
- textContent 只处理纯文本,性能更好,更安全
-
如何优化 DOM 操作性能?
- 减少 DOM 访问次数,缓存查询结果
- 使用文档片段批量操作
- 避免强制同步布局
- 使用事件委托
- 使用 requestAnimationFrame 批量更新样式
-
什么是重排和重绘?
- 重排(Reflow):元素尺寸、位置变化,浏览器重新计算布局
- 重绘(Repaint):元素外观变化(颜色、背景等),不影响布局
- 重排一定导致重绘,重绘不一定导致重排