DOM操作
# 14.DOM操作
DOM(Document Object Model)是 JavaScript 操作网页的核心 API。浏览器将 HTML 解析为树形结构的节点对象,JavaScript 通过 DOM API 动态修改页面内容、结构和样式。掌握 DOM 操作是前端开发的基本功。
# 14.1 DOM 树结构
# 14.1.1 节点类型
DOM 中一切皆节点(Node),常见的节点类型:
| 节点类型 | nodeType | nodeName 示例 | 说明 |
|---|---|---|---|
| Element | 1 | 'DIV' | HTML 元素 |
| Text | 3 | '#text' | 文本内容 |
| Comment | 8 | '#comment' | 注释 |
| Document | 9 | '#document' | 文档根节点 |
| DocumentFragment | 11 | '#document-fragment' | 文档片段 |
const div = document.createElement('div');
console.log(div.nodeType); // 1
console.log(div.nodeName); // 'DIV'
const text = document.createTextNode('hello');
console.log(text.nodeType); // 3
// 检查节点类型
if (node.nodeType === Node.ELEMENT_NODE) {
// 是元素节点
}
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
# 14.1.2 节点关系
// 假设 HTML:<ul id="list"><li>A</li><li>B</li><li>C</li></ul>
const list = document.getElementById('list');
// 父节点
list.parentNode; // 父节点(可能是任何类型)
list.parentElement; // 父元素节点(跳过非元素节点)
// 子节点
list.childNodes; // NodeList(包含文本节点、注释等)
list.children; // HTMLCollection(仅元素节点)
list.firstChild; // 第一个子节点(可能是文本)
list.firstElementChild; // 第一个子元素
list.lastChild;
list.lastElementChild;
// 兄弟节点
const li = list.children[1]; // 第二个 <li>
li.previousSibling; // 前一个节点(可能是文本)
li.previousElementSibling; // 前一个元素
li.nextSibling;
li.nextElementSibling;
// 子元素数量
list.childElementCount; // 3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
注意:HTML 中的空白和换行会生成文本节点,使用
children、firstElementChild等属性可以跳过这些文本节点。
# 14.1.3 Document 对象
// 文档信息
document.title; // 页面标题
document.URL; // 当前 URL
document.domain; // 域名
document.referrer; // 来源页面
document.readyState; // 'loading' | 'interactive' | 'complete'
document.characterSet; // 'UTF-8'
document.contentType; // 'text/html'
// 特殊元素引用
document.documentElement; // <html>
document.head; // <head>
document.body; // <body>
document.forms; // 所有 <form>
document.images; // 所有 <img>
document.links; // 所有 <a href>
document.scripts; // 所有 <script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 14.2 元素查找
# 14.2.1 基础查找方法
// 通过 ID(返回单个元素)
const el = document.getElementById('myId');
// 通过类名(返回 HTMLCollection,实时更新)
const items = document.getElementsByClassName('item');
// 通过标签名(返回 HTMLCollection)
const divs = document.getElementsByTagName('div');
// 通过 name 属性
const inputs = document.getElementsByName('email');
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
# 14.2.2 CSS 选择器查找
现代开发推荐使用 querySelector 系列:
// 返回第一个匹配元素
document.querySelector('#myId');
document.querySelector('.item');
document.querySelector('div.container > p:first-child');
document.querySelector('[data-id="123"]');
// 返回所有匹配元素(静态 NodeList)
document.querySelectorAll('.item');
document.querySelectorAll('input[type="text"]');
// 在特定元素内查找
const container = document.querySelector('.container');
container.querySelector('.child');
container.querySelectorAll('li');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
HTMLCollection vs NodeList:
| 特性 | HTMLCollection | NodeList |
|---|---|---|
| 返回方法 | getElementsBy* | querySelectorAll |
| 实时性 | 实时更新 | 静态快照 |
| 包含类型 | 仅元素 | 所有节点 |
forEach | 不支持 | 支持 |
| 索引访问 | [i] 或 item(i) | [i] 或 item(i) |
// HTMLCollection 是实时的
const items = document.getElementsByClassName('item');
console.log(items.length); // 3
document.querySelector('.item').remove();
console.log(items.length); // 2(自动更新!)
// NodeList(querySelectorAll)是静态的
const nodes = document.querySelectorAll('.item');
// 即使 DOM 变化,nodes 不变
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
# 14.2.3 closest 和 matches
// matches - 检查元素是否匹配选择器
const el = document.querySelector('li');
el.matches('.active'); // true/false
el.matches('ul > li'); // true/false
// closest - 向上查找最近的匹配祖先(含自身)
el.closest('.container'); // 最近的 .container 祖先
el.closest('ul'); // 最近的 <ul> 祖先
el.closest('li'); // 自身(如果匹配)
// 实际应用:事件委托中定位目标元素
document.addEventListener('click', (e) => {
const button = e.target.closest('button[data-action]');
if (button) {
const action = button.dataset.action;
handleAction(action);
}
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 14.3 元素创建与操作
# 14.3.1 创建节点
// 创建元素
const div = document.createElement('div');
div.className = 'container';
div.id = 'main';
// 创建文本
const text = document.createTextNode('Hello World');
// 创建文档片段(用于批量操作)
const fragment = document.createDocumentFragment();
// 克隆节点
const clone = div.cloneNode(false); // 浅克隆(不含子节点)
const deep = div.cloneNode(true); // 深克隆(含子节点)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
# 14.3.2 插入节点
const parent = document.querySelector('.container');
const newEl = document.createElement('div');
// 末尾追加
parent.appendChild(newEl);
// 在参考节点前插入
const ref = parent.children[1];
parent.insertBefore(newEl, ref);
// 现代 API(更灵活)
parent.append(newEl, 'text', anotherEl); // 末尾追加(支持多个/文本)
parent.prepend(newEl); // 开头插入
ref.before(newEl); // 在 ref 前面
ref.after(newEl); // 在 ref 后面
// insertAdjacentHTML - 解析 HTML 字符串并插入
parent.insertAdjacentHTML('beforebegin', '<p>Before</p>'); // 元素前面
parent.insertAdjacentHTML('afterbegin', '<p>First</p>'); // 元素内部开头
parent.insertAdjacentHTML('beforeend', '<p>Last</p>'); // 元素内部末尾
parent.insertAdjacentHTML('afterend', '<p>After</p>'); // 元素后面
// insertAdjacentElement / insertAdjacentText
parent.insertAdjacentElement('beforeend', newEl);
parent.insertAdjacentText('beforeend', 'some text');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 14.3.3 删除和替换
// 删除
const el = document.querySelector('.to-remove');
el.remove(); // 现代方式
el.parentNode.removeChild(el); // 传统方式
// 替换
const oldEl = document.querySelector('.old');
const newEl = document.createElement('div');
oldEl.replaceWith(newEl); // 现代方式
oldEl.parentNode.replaceChild(newEl, oldEl); // 传统方式
// 清空所有子节点
parent.innerHTML = ''; // 简单但有安全隐患
parent.textContent = ''; // 更安全
parent.replaceChildren(); // 最现代的方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 14.3.4 DocumentFragment 批量操作
使用 DocumentFragment 避免多次重排(reflow):
// 不推荐:每次 appendChild 都触发重排
const list = document.querySelector('#list');
for (let i = 0; i < 1000; i++) {
const li = document.createElement('li');
li.textContent = `Item ${i}`;
list.appendChild(li); // 触发 1000 次重排
}
// 推荐:DocumentFragment
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const li = document.createElement('li');
li.textContent = `Item ${i}`;
fragment.appendChild(li);
}
list.appendChild(fragment); // 只触发 1 次重排
// 或使用 replaceChildren
const items = Array.from({ length: 1000 }, (_, i) => {
const li = document.createElement('li');
li.textContent = `Item ${i}`;
return li;
});
list.replaceChildren(...items);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 14.4 属性操作
# 14.4.1 标准属性
HTML 标准属性直接映射为 DOM 属性:
const input = document.querySelector('input');
// 读写属性
input.id = 'username';
input.type = 'text';
input.value = 'hello';
input.disabled = true;
input.checked = false;
// class 属性比较特殊
input.className = 'form-input active';
// href 返回完整 URL
const a = document.querySelector('a');
a.href; // 'https://example.com/page'(完整URL)
a.getAttribute('href'); // '/page'(原始值)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 14.4.2 classList API
const el = document.querySelector('.box');
el.classList.add('active', 'visible');
el.classList.remove('hidden');
el.classList.toggle('active'); // 切换
el.classList.toggle('active', true); // 强制添加
el.classList.toggle('active', false); // 强制移除
el.classList.contains('active'); // 检查
el.classList.replace('old', 'new'); // 替换
// 遍历
for (const cls of el.classList) {
console.log(cls);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
# 14.4.3 dataset 自定义数据属性
// HTML: <div data-user-id="123" data-role="admin">
const el = document.querySelector('div');
// 读取(自动转为 camelCase)
el.dataset.userId; // '123'
el.dataset.role; // 'admin'
// 设置
el.dataset.newAttr = 'value';
// → <div data-new-attr="value">
// 删除
delete el.dataset.role;
// 遍历
for (const [key, value] of Object.entries(el.dataset)) {
console.log(key, value);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 14.4.4 通用属性方法
// getAttribute / setAttribute - 操作 HTML 属性(字符串)
el.getAttribute('data-id');
el.setAttribute('aria-label', 'Close');
el.removeAttribute('hidden');
el.hasAttribute('disabled');
// 获取所有属性
for (const attr of el.attributes) {
console.log(attr.name, attr.value);
}
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
# 14.5 样式操作
# 14.5.1 内联样式
const el = document.querySelector('.box');
// style 属性(只能读写内联样式)
el.style.color = 'red';
el.style.backgroundColor = '#fff'; // camelCase
el.style.fontSize = '16px';
el.style.cssText = 'color: red; font-size: 16px;'; // 批量设置
// 移除内联样式
el.style.color = '';
el.style.removeProperty('background-color'); // kebab-case
// 读取内联样式
el.style.color; // 'red'(仅内联样式)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
# 14.5.2 计算样式
getComputedStyle 获取元素最终应用的样式(包含继承和 CSS 规则):
const el = document.querySelector('.box');
const computed = getComputedStyle(el);
computed.color; // 'rgb(255, 0, 0)'
computed.fontSize; // '16px'
computed.display; // 'block'
computed.getPropertyValue('font-size'); // '16px'
// 获取伪元素样式
const pseudoStyle = getComputedStyle(el, '::before');
pseudoStyle.content; // '"text"'
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
# 14.5.3 CSS 变量操作
// 设置 CSS 变量
document.documentElement.style.setProperty('--primary-color', '#007bff');
document.documentElement.style.setProperty('--font-size', '16px');
// 读取 CSS 变量
const root = getComputedStyle(document.documentElement);
root.getPropertyValue('--primary-color'); // '#007bff'
// 在元素级别覆盖
el.style.setProperty('--primary-color', '#ff0000');
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
# 14.5.4 操作样式表
// 访问样式表
document.styleSheets; // StyleSheetList
const sheet = document.styleSheets[0];
sheet.cssRules; // CSSRuleList
// 动态添加规则
sheet.insertRule('.new-class { color: red; }', sheet.cssRules.length);
sheet.deleteRule(0);
// 创建新样式表
const style = document.createElement('style');
style.textContent = `
.dynamic { color: blue; }
`;
document.head.appendChild(style);
// 使用 CSSStyleSheet 构造器(现代方式)
const newSheet = new CSSStyleSheet();
newSheet.replaceSync('.modern { color: green; }');
document.adoptedStyleSheets = [...document.adoptedStyleSheets, newSheet];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 14.6 元素尺寸与位置
# 14.6.1 尺寸属性
const el = document.querySelector('.box');
// clientWidth/Height - 内容 + padding(不含 border 和 scrollbar)
el.clientWidth;
el.clientHeight;
// offsetWidth/Height - 内容 + padding + border + scrollbar
el.offsetWidth;
el.offsetHeight;
// scrollWidth/Height - 内容完整尺寸(包括溢出部分)
el.scrollWidth;
el.scrollHeight;
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
盒模型尺寸关系:
┌───────────────────── offsetWidth ─────────────────────┐
│ border │
│ ┌─────────────── clientWidth ──────────────┐ │
│ │ padding │scrollbar│
│ │ ┌──────── content width ────────┐ │ │
│ │ │ │ │ │
│ │ └───────────────────────────────┘ │ │
│ └──────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
# 14.6.2 位置属性
// offsetTop/Left - 相对于 offsetParent 的偏移
el.offsetTop;
el.offsetLeft;
el.offsetParent; // 最近的定位祖先
// scrollTop/Left - 滚动偏移(可读写)
el.scrollTop;
el.scrollLeft;
el.scrollTop = 0; // 滚动到顶部
// getBoundingClientRect - 相对于视口的精确位置
const rect = el.getBoundingClientRect();
rect.top; // 元素顶部到视口顶部的距离
rect.bottom; // 元素底部到视口顶部的距离
rect.left; // 元素左侧到视口左侧的距离
rect.right; // 元素右侧到视口左侧的距离
rect.width; // 元素宽度(= right - left)
rect.height; // 元素高度(= bottom - top)
rect.x; // 同 left
rect.y; // 同 top
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 14.6.3 滚动操作
// 滚动到指定位置
window.scrollTo({ top: 0, behavior: 'smooth' });
el.scrollTo({ top: 100, left: 0, behavior: 'smooth' });
// 滚动指定距离
window.scrollBy({ top: 100, behavior: 'smooth' });
// 滚动元素到可见区域
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
// block: 'start' | 'center' | 'end' | 'nearest'
// inline: 'start' | 'center' | 'end' | 'nearest'
// 视口尺寸
window.innerWidth; // 视口宽度(含滚动条)
window.innerHeight; // 视口高度
document.documentElement.clientWidth; // 视口宽度(不含滚动条)
document.documentElement.clientHeight;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 14.6.4 判断元素可见
// 方法1:getBoundingClientRect
function isInViewport(el) {
const rect = el.getBoundingClientRect();
return (
rect.top < window.innerHeight &&
rect.bottom > 0 &&
rect.left < window.innerWidth &&
rect.right > 0
);
}
// 方法2:IntersectionObserver(推荐)
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
console.log('元素可见:', entry.target);
console.log('可见比例:', entry.intersectionRatio);
}
});
}, {
root: null, // 视口
rootMargin: '0px', // 提前触发的边距
threshold: [0, 0.5, 1] // 触发回调的可见比例
});
observer.observe(el);
observer.unobserve(el);
observer.disconnect();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 14.7 MutationObserver
MutationObserver 监听 DOM 变化,替代已废弃的 Mutation Events:
# 14.7.1 基本用法
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
switch (mutation.type) {
case 'childList':
console.log('子节点变化');
console.log('添加:', mutation.addedNodes);
console.log('删除:', mutation.removedNodes);
break;
case 'attributes':
console.log(`属性 ${mutation.attributeName} 变化`);
console.log('旧值:', mutation.oldValue);
break;
case 'characterData':
console.log('文本内容变化');
console.log('旧值:', mutation.oldValue);
break;
}
}
});
// 配置观察选项
observer.observe(document.body, {
childList: true, // 监听子节点增删
attributes: true, // 监听属性变化
characterData: true, // 监听文本内容变化
subtree: true, // 监听后代节点
attributeOldValue: true, // 记录属性旧值
characterDataOldValue: true, // 记录文本旧值
attributeFilter: ['class', 'style'], // 只监听指定属性
});
// 停止观察
observer.disconnect();
// 获取已排队但未处理的变化
const pending = observer.takeRecords();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 14.7.2 实际应用
监听 DOM 变化实现自动绑定:
// 自动为新增的懒加载图片添加 IntersectionObserver
const imgObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
imgObserver.unobserve(img);
}
});
});
const domObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.nodeType === Node.ELEMENT_NODE) {
// 检查新增的元素及其后代
const lazyImages = node.matches('img[data-src]')
? [node]
: node.querySelectorAll('img[data-src]');
lazyImages.forEach(img => imgObserver.observe(img));
}
}
}
});
domObserver.observe(document.body, { childList: true, subtree: true });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 14.8 ResizeObserver
监听元素尺寸变化:
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
const { width, height } = entry.contentRect;
console.log(`${entry.target.id}: ${width}x${height}`);
// borderBoxSize(更精确)
const borderBox = entry.borderBoxSize[0];
console.log(`Border box: ${borderBox.inlineSize}x${borderBox.blockSize}`);
}
});
observer.observe(document.querySelector('.responsive-box'));
// 应用:响应式组件
const container = document.querySelector('.container');
const resizeObserver = new ResizeObserver(entries => {
const { width } = entries[0].contentRect;
if (width < 600) {
container.classList.add('compact');
} else {
container.classList.remove('compact');
}
});
resizeObserver.observe(container);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 14.9 性能优化
# 14.9.1 减少重排(Reflow)
触发重排的操作:修改元素几何属性(width、height、padding、margin)、添加删除元素、读取布局信息(offsetTop、getBoundingClientRect)。
// 不推荐:读写交替触发多次重排
el.style.width = '100px';
const h = el.offsetHeight; // 强制重排
el.style.height = h + 'px';
// 推荐:先批量读,再批量写
const h = el.offsetHeight; // 读
el.style.width = '100px'; // 写
el.style.height = h + 'px'; // 写(合并为一次重排)
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
# 14.9.2 requestAnimationFrame
DOM 动画应使用 requestAnimationFrame 替代 setInterval:
function animate(element, target) {
let current = 0;
function step() {
current += 2;
element.style.transform = `translateX(${current}px)`;
if (current < target) {
requestAnimationFrame(step);
}
}
requestAnimationFrame(step);
}
animate(document.querySelector('.box'), 300);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 14.9.3 虚拟 DOM 的核心思想
// 虚拟 DOM 是 JavaScript 对象,描述真实 DOM 结构
const vnode = {
tag: 'div',
props: { class: 'container' },
children: [
{ tag: 'p', props: {}, children: ['Hello'] },
{ tag: 'p', props: {}, children: ['World'] },
]
};
// 渲染函数:vnode → 真实 DOM
function render(vnode) {
if (typeof vnode === 'string') {
return document.createTextNode(vnode);
}
const el = document.createElement(vnode.tag);
for (const [key, value] of Object.entries(vnode.props || {})) {
el.setAttribute(key, value);
}
for (const child of vnode.children || []) {
el.appendChild(render(child));
}
return el;
}
// 简化的 diff 算法思路:
// 比较新旧 vnode 树,找出差异,只更新变化的部分
// 这就是 React/Vue 的核心原理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 14.9.4 DOM 操作性能建议
- 减少 DOM 访问次数:缓存 DOM 引用和计算结果
- 批量修改:使用 DocumentFragment 或
innerHTML - 避免强制同步布局:不要读写交替
- 使用 CSS 类切换替代逐个修改 style 属性
- 使用事件委托替代大量事件绑定
- 使用
transform和opacity做动画(不触发重排) - 使用
will-change提示浏览器优化 - 使用
content-visibility: auto(CSS)跳过屏幕外元素的渲染
上次更新: 2026/06/10, 11:13:41