返回首页

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 标签(如 divp
  • 文本节点:标签内的文本内容
  • 属性节点:元素的属性(如 titleclass
  • 注释节点: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';
});

最佳实践

  1. 缓存 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';
    }
    
  2. 使用事件委托减少监听器数量

    // ❌ 为每个子元素添加监听器
    document.querySelectorAll('li').forEach(li => {
        li.addEventListener('click', handler);
    });
    
    // ✅ 使用事件委托
    document.getElementById('list').addEventListener('click', e => {
        if (e.target.tagName === 'LI') {
            handler(e);
        }
    });
    
  3. 批量操作 DOM 时使用文档片段

  4. 使用 classList 代替直接操作 className

  5. 避免在循环中使用 innerHTML +=

面试要点

  1. DOM 操作的分类?

    • 创建节点:createElement、createTextNode、createDocumentFragment
    • 查询节点:querySelector、querySelectorAll、getElementById 等
    • 更新节点:innerHTML、textContent、style、setAttribute
    • 添加节点:appendChild、insertBefore、append、prepend
    • 删除节点:removeChild、remove
  2. querySelector 和 getElementById 的区别?

    • querySelector 返回第一个匹配的元素,支持 CSS 选择器
    • getElementById 只通过 ID 查找,效率更高
    • querySelectorAll 返回静态 NodeList,getElementsByTagName/ClassName 返回动态 HTMLCollection
  3. innerHTML 和 textContent 的区别?

    • innerHTML 解析 HTML 标签,有 XSS 风险
    • textContent 只处理纯文本,性能更好,更安全
  4. 如何优化 DOM 操作性能?

    • 减少 DOM 访问次数,缓存查询结果
    • 使用文档片段批量操作
    • 避免强制同步布局
    • 使用事件委托
    • 使用 requestAnimationFrame 批量更新样式
  5. 什么是重排和重绘?

    • 重排(Reflow):元素尺寸、位置变化,浏览器重新计算布局
    • 重绘(Repaint):元素外观变化(颜色、背景等),不影响布局
    • 重排一定导致重绘,重绘不一定导致重排