浏览器中的每个页面都是一棵由节点构成的树,这棵树的编程接口就是 DOM。无论你使用原生 JavaScript 还是 React、Vue 等现代框架,最终对页面内容的每一次增删改查,都离不开 HTML DOM API。然而,许多开发者对 DOM 的理解停留在“会用”层面——知道 querySelector 却分不清 HTMLCollection 与 NodeList 的动态性差异;习惯用 innerHTML 却忽略了 DocumentFragment 的性能优势;熟悉 addEventListener 却不了解 MutationObserver 的监听能力。
本文以 MDN 权威文档为基准,系统梳理 DOM 的本质、四层核心接口的继承关系、元素查询与集合类型、属性与特性的区别、事件委托、性能优化技巧以及 MutationObserver 与 Shadow DOM 等现代特性。通过清晰的逻辑、对比表格和实战代码,帮助你构建一套准确、完整的 DOM 知识体系。
一、DOM 的本质:文档的树形抽象
DOM(Document Object Model,文档对象模型)是浏览器将 HTML 或 XML 文档解析后生成的一个树形结构。它提供了编程接口,允许脚本动态访问和修改文档的内容、结构与样式。
每个 HTML 文档加载后,会产生一个 Document 对象作为树的根节点。树中的每个组成部分都称为一个“节点”(Node)。例如:
<html> <head><title>示例</title></head> <body> <div>Hello</div> <ul><li>项1</li></ul> </body></html>
其 DOM 树结构(简化)如下:
Document└── html (Element) ├── head (Element) │ └── title (Element) │ └── text (Text) └── body (Element) ├── div (Element) │ └── text (Text) └── ul (Element) └── li (Element) └── text (Text)
节点类型众多,日常开发中最常用的是 元素节点(Element) 和 文本节点(Text)。
二、核心接口的继承层次与职责
理解 DOM 的关键在于理清 Node → Element → HTMLElement → 具体元素以及 Document 这四类接口的职责划分。
继承关系图
Node (抽象基类)├── Document ├── Element │ ├── HTMLElement │ │ ├── HTMLDivElement│ │ ├── HTMLInputElement│ │ └── ...(其他具体元素)│ └── SVGElement └── Text
1. Node —— 所有节点的始祖
Node 是最底层的抽象接口,任何类型的节点(元素、文本、注释、文档)都实现了它。它主要提供 节点树中通用的关系导航 和 基本操作。
const parent = node.parentNode; const children = node.childNodes;
注意:childNodes 会包含文本节点(如换行符和空格),而不仅仅是元素节点。
2. Element —— 元素节点的专用接口
属性操作:getAttribute()、setAttribute()、removeAttribute()。
类名与样式:classList、style、className。
元素间导航(忽略文本节点):children(返回 HTMLCollection)、firstElementChild、nextElementSibling。
element.setAttribute('data-id', '123');element.classList.add('active');for (let child of element.children) { }
3. HTMLElement —— 所有 HTML 元素的共同父类
HTMLElement 继承自 Element,为所有 HTML 元素(<div>、<input>、<button> 等)提供了通用的属性和方法。具体的元素接口(如 HTMLInputElement)会进一步扩展自己特有的属性(如 value、checked)。
div.hidden = true; console.log(div.offsetHeight); input.value = '文本';
4. Document —— 文档的总入口
Document 代表整个 HTML 文档,是 DOM 树的根节点(同时也是 window 对象的属性)。它提供文档级别的全局功能。
文档信息:title、URL、cookie、referrer。
创建节点:createElement()、createTextNode()、createDocumentFragment()。
查询元素:getElementById()、querySelector()、querySelectorAll()。
快捷属性:document.body、document.documentElement(<html>)、document.head。
document.title = '新标题';const div = document.createElement('div');const app = document.getElementById('app');
快速记忆
接口 | 一句话职责 | 典型使用场景 |
Node | 通用节点关系 | parentNode、childNodes、appendChild |
Element | 元素属性与子元素 | getAttribute、classList、children |
HTMLElement | HTML 元素通用行为 | hidden、offsetHeight、innerText |
Document | 文档全局入口 | createElement、getElementById、title |
三、元素查询方法与集合类型
DOM 提供了多种查询元素的方法,但它们的返回值类型和行为差异常被忽略。
方法 / 属性 | 返回类型 | 动态性 | 包含内容 |
getElementById() | 单个元素或 null | — | 元素 |
getElementsByClassName() | HTMLCollection | 动态 | 仅元素 |
getElementsByTagName() | HTMLCollection | 动态 | 仅元素 |
getElementsByName() | NodeList(通常静态) | 视浏览器而定 | 元素 |
querySelector() | 单个元素或 null | — | 元素 |
querySelectorAll() | NodeList | 静态 | 元素 |
element.children | HTMLCollection | 动态 | 仅元素 |
element.childNodes | NodeList | 动态 | 所有节点 |
动态集合 vs 静态集合
const divs = document.getElementsByTagName('div'); for (let i = 0; i < divs.length; i++) { const newDiv = document.createElement('div'); document.body.appendChild(newDiv); }
const staticDivs = document.querySelectorAll('div');
遍历与转换
四、创建、修改与删除元素
创建节点
const div = document.createElement('div'); const text = document.createTextNode('文本'); const fragment = document.createDocumentFragment();
修改内容
element.textContent = '纯文本'; element.innerHTML = '<span>带标签的内容</span>';
插入与移除
传统方法(兼容性好):
parent.appendChild(child);parent.insertBefore(newNode, refNode);parent.removeChild(child);parent.replaceChild(newNode, oldChild);
现代方法(简洁直观,IE 不支持):
parent.append(newNode1, newNode2); parent.prepend(newNode); child.before(newNode); child.after(newNode); child.replaceWith(newNode); child.remove();
五、Attribute 与 Property 的本质区别
这一概念经常令人困惑,但厘清后能避免大量 bug。
对于标准属性(如 id、class、value),Attribute 与 Property 通常会同步,但以下情况需特别注意:
最佳实践:优先使用 Property,除非你需要读取 HTML 中原始的 Attribute 值(例如通过 getAttribute 获取未曾改变的 value)。对于自定义数据,推荐使用 dataset。
// 推荐方式input.disabled = true;div.dataset.userId = '123';
// 而非 input.setAttribute('disabled', 'disabled');
六、事件处理与事件委托
标准事件监听
element.addEventListener('click', (event) => { console.log(event.target); console.log(event.currentTarget); event.preventDefault(); event.stopPropagation(); });
事件流顺序:捕获阶段 → 目标阶段 → 冒泡阶段。第三个参数可传入 true 或 { capture: true } 以在捕获阶段触发。
事件委托
利用事件冒泡机制,在父元素上统一监听子元素的事件。优点:
减少事件监听器数量,提升性能。
自动处理动态添加的子元素,无需重新绑定。
document.querySelector('.todo-list').addEventListener('click', (e) => { const deleteBtn = e.target.closest('.delete-btn'); if (deleteBtn) { deleteBtn.closest('.todo-item').remove(); }});
closest 方法向上查找匹配选择器的祖先元素,比直接判断 target 更加健壮。
七、DOM 遍历与关系导航
const allChildren = parent.childNodes; const elementChildren = parent.children;
const nextElement = element.nextElementSibling;const prevElement = element.previousElementSibling;
const parentElement = element.parentElement;
八、性能优化:编写高效的 DOM 操作
DOM 操作是浏览器中最昂贵的操作之一,每次修改都可能触发 重排(reflow) 和 重绘(repaint)。遵循以下原则可显著提升性能。
1. 使用 DocumentFragment 批量插入
DocumentFragment 是一个存在于内存中的文档片段,将其作为临时容器批量添加子节点,最后一次性插入真实 DOM,将多次重排减少到一次。
const fragment = document.createDocumentFragment();for (let i = 0; i < 1000; i++) { const li = document.createElement('li'); li.textContent = `Item ${i}`; fragment.appendChild(li);}ul.appendChild(fragment);
2. 读写分离,避免布局抖动
交替读取布局属性(如 offsetHeight、getComputedStyle)和修改样式会导致浏览器强制同步布局。应先将所有读取操作集中,再统一写入。
elements.forEach(el => { const h = el.offsetHeight; el.style.height = h + 10; });
const heights = elements.map(el => el.offsetHeight);elements.forEach((el, i) => { el.style.height = heights[i] + 10;});
3. 使用 classList 切换样式,避免逐个修改内联样式
el.style.width = '100px';el.style.height = '100px';el.style.background = 'red';
el.classList.add('highlight-box');
4. 对高频事件使用防抖或节流
input.addEventListener('input', debounce(handleSearch, 300));window.addEventListener('scroll', throttle(handleScroll, 100));
九、高级 API:
MutationObserver 与 Shadow DOM
1. MutationObserver —— 高效监听 DOM 变化
传统的 MutationEvent 已废弃,MutationObserver 提供异步、批量的监听能力,性能更优。
const observer = new MutationObserver((mutations) => { mutations.forEach(m => { if (m.type === 'childList') { console.log('子节点变化', m.addedNodes, m.removedNodes); } else if (m.type === 'attributes') { console.log(`属性变化: ${m.attributeName}`); } });});observer.observe(targetElement, { childList: true, attributes: true, subtree: true });observer.disconnect();
典型应用场景:监听动态加载的评论区内容、自动保存编辑器修改、检测第三方脚本对 DOM 的非预期修改。
2. Shadow DOM —— 样式与 DOM 的封装
Shadow DOM 是 Web Components 的核心技术之一,允许将一棵独立的 DOM 树附加到常规元素上,实现样式隔离和 DOM 封装。
const host = document.querySelector('#widget');const shadow = host.attachShadow({ mode: 'open' });shadow.innerHTML = ` <style>p { color: purple; }</style> <p>外部样式不会影响我,我也不会泄漏出去</p>`;
总结
HTML DOM API 是前端开发无法绕过的底层技术。掌握它的关键在于:
理清接口层次:Node → Element → HTMLElement → 具体元素,以及独立的 Document。每个接口有明确的职责范围——Node 管节点关系,Element 管属性与子元素,HTMLElement 提供 HTML 特有属性,Document 是全局入口。
区分集合类型:HTMLCollection 动态且仅含元素;NodeList 可能静态(querySelectorAll)或动态(childNodes)。遍历时注意动态集合的陷阱。
正确选择 API:Attribute vs Property,textContent vs innerHTM`,children vs childNodes。
性能意识:使用 DocumentFragment、读写分离、事件委托、防抖节流。
拥抱现代能力:MutationObserver 监听 DOM 变化,Shadow DOM 实现样式隔离。
无论你使用何种框架,扎实的 DOM 基础都能让你在调试性能瓶颈、实现复杂交互时游刃有余。希望本文能帮助你系统性地建立这份功底。
阅读原文:https://mp.weixin.qq.com/s/N7G5DLtoZIrpE91vfIf6FQ
该文章在 2026/5/20 17:40:52 编辑过