1. Selectors API(简要介绍)
Selectors API 是把 CSS 选择器直接用于 DOM 查找的统一接口,核心三个方法:
querySelector(selector)
返回匹配selector的第一个元素(document 或任意 Element 上可用)。如果没有返回null。适合查找单个元素或快速选择。querySelectorAll(selector)
返回所有匹配元素的 静态NodeList(快照)。注意:返回的是静态节点集合,不会随着 DOM 修改自动更新。遍历或反复查询时注意性能。matches(selector)
检查当前元素是否匹配给定选择器,常用于事件委托中确认事件目标是否匹配。现代浏览器统一为matches,历史上有前缀webkitMatchesSelector/msMatchesSelector等,可做兼容写法。
示例:
dart
const firstBtn = document.querySelector('.btn.primary');
const allItems = document.querySelectorAll('.list > li');
if (someElement.matches(':scope > .active')) { /* ... */ }
性能建议:对于简单单一查找(如按 id)优先 getElementById;对大量选择或频繁操作,缓存查询结果或限制作用域(在容器 Element 上调用)可减少重绘/查询成本。
2. 元素遍历(通过这些 API 遍历树形)
核心属性/方法(只列元素节点相关,避免 text/comment):
element.children--- 返回子元素集合(HTMLCollection,是 live 的,DOM 变化会反映)。element.firstElementChild/element.lastElementChild--- 第一个/最后一个子元素(或null)。element.nextElementSibling/element.previousElementSibling--- 同级的下/上一个元素节点(或null)。
常见遍历模式(深度优先,元素节点):
scss
// 递归 DFS 遍历所有元素节点
function walk(el, cb) {
cb(el);
for (let child of el.children) walk(child, cb);
}
walk(document.body, el => console.log(el.tagName));
// 使用 nextElementSibling 遍历同级
let node = container.firstElementChild;
while (node) {
// 处理 node
node = node.nextElementSibling;
}
补充工具(更强):TreeWalker / NodeIterator(用于复杂遍历过滤)------当你要在大量节点上做带过滤的遍历,TreeWalker 更高效并且语义更清晰。
注意 childNodes 与 children 的区别:childNodes 包含文本和注释节点;children 只包含元素节点(更常用)。
3. HTML5(与 DOM 扩展相关的要点)
HTML5 对 DOM 的扩展很多,这里重点和实用 API 相关的:
- 新语义元素(
<section>,<article>,<nav>等)------对语义化、可访问性有利。 dataset(data-*)与classList、querySelector全面支持,使 DOM 操作更灵活。contentEditable、historyAPI(pushState/replaceState)及canvas、audio、video等增强了页面交互能力。document.head、document.body等属性更规范、便于访问。
(HTML5 范围广,上面是与 DOM 直接相关的重要点。)
4. CSS 类扩展
4.1 getElementsByClassName
- 返回的是 live
HTMLCollection(会随 DOM 变动自动更新)。 - 速度通常比
querySelectorAll快(具体受实现影响),适合需要实时反映的场景,但在修改 DOM 的循环中请小心 live 集合带来的副作用(建议先做Array.from()快照)。
4.2 classList 属性(DOMTokenList)
-
现代推荐操作类名的 API,语义清晰:
el.classList.add('a','b')el.classList.remove('c')el.classList.toggle('d')或el.classList.toggle('e', condition)el.classList.contains('f')el.classList.replace('old','new')
-
classList性能优于直接字符串拼接(避免错误空格、重复),并支持forEach。
示例:
csharp
el.classList.add('is-visible');
if (!el.classList.contains('active')) el.classList.add('active');
el.classList.toggle('expanded', shouldExpand);
5. 焦点管理(focus)
关键点与最佳实践:
-
API:
element.focus([options])、element.blur()、document.activeElement。element.focus({preventScroll: true})(可选)可聚焦同时不触发滚动(现代浏览器支持)。
-
tabindex:tabindex="-1":可以程序聚焦,但不可通过 Tab 导航。tabindex="0":参与顺序,位置由文档流决定(常用于自定义可聚焦组件)。- 正整数 tabindex(
>0)会创建自定义顺序,不推荐使用(维护困难)。
-
可访问性(A11y):确保可视焦点样式(不要随意移除 outline),模态对话框需实现焦点陷阱(trap focus)和返回焦点。
-
焦点陷阱示例(简单):
ini
const focusable = modal.querySelectorAll('a, button, input, [tabindex]:not([tabindex="-1"])');
let first = focusable[0], last = focusable[focusable.length-1];
modal.addEventListener('keydown', e => {
if (e.key === 'Tab') {
if (e.shiftKey && document.activeElement === first) {
e.preventDefault(); last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault(); first.focus();
}
}
});
6. HTMLDocument 扩展
document.readyState:常见值loading/interactive/complete。在interactive时 DOM 已可操作但外部资源(图片)可能未完全加载;DOMContentLoaded触发时通常为interactive。document.compatMode:"CSS1Compat"(标准模式)或"BackCompat"(怪异模式)。用于判断文档是否以标准渲染。document.head:直接访问<head>节点的快捷属性(等价于document.getElementsByTagName('head')[0])。
实践:在绑定 DOMContentLoaded 时,可先检查 document.readyState !== 'loading' 以避免重复等待。
7. 字符集属性(characterSet 详解)
document.characterSet(有时也见document.charset)返回文档使用的字符编码字符串,例如"UTF-8"。- 这是只读的,决定于 HTTP 头或文档内
<meta charset="...">。解析阶段确定,运行时更改不会重新解析页面。 - 用途:调试/检测文本编码问题或服务器端生成页面时确认编码;一般无需频繁使用。
8. 自定义数据属性(data-*)
- HTML:
<div data-user-id="123" data-x-y-z="value"></div> - JS:通过
element.dataset访问(DOMStringMap),属性名由kebab-case转为camelCase:data-user-id→dataset.userId,data-x-y-z→dataset.xYZ(注意大小写转换)。 - 值为字符串;如果要存对象/数组,需序列化(
JSON.stringify/JSON.parse)。 - 注意:不要把敏感数据(令牌、密码)放到 DOM 可见属性上------它对前端所有脚本和恶意扩展可见。
示例:
ini
el.dataset.userId = '123';
console.log(el.dataset.xYz); // 读取 data-x-y-z
9. 插入标记(innerHTML / outerHTML / insertAdjacent* / 性能与安全)
9.1 innerHTML
- 读取返回元素内部的 HTML 字符串,赋值会 替换子节点 并重新解析 HTML。
- 优点:简单、直观。缺点:可能导致大量重排、事件监听丢失(因为旧节点被销毁)、XSS 风险。
- 脚本插入:通过
innerHTML插入<script>通常不会 直接执行(不同环境历史行为有差异),若需执行脚本应使用createElement('script')并 append。
9.2 旧 IE 中的 innerHTML
- 旧 IE(历史)在对
table、select等元素的innerHTML支持上有奇怪差异(例如自动插入tbody),现代浏览器已较统一。对兼容性要求高的项目需注意旧浏览器行为。
9.3 outerHTML
- 代表元素自身及其内部的 HTML 字符串。赋值会替换当前元素(包括自身)。
9.4 insertAdjacentHTML 与 insertAdjacentText
-
element.insertAdjacentHTML(position, htmlString):在指定位置插入解析后的节点。position为:'beforebegin','afterbegin','beforeend','afterend'
-
insertAdjacentText是在指定位置插入纯文本(不会解析为 HTML),更安全。 -
insertAdjacentHTML相比innerHTML的优点:不会替换父节点的全部内容,通常性能更好且保留现有节点和监听器(插入位置明确),常用作局部更新。
示例:
arduino
container.insertAdjacentHTML('beforeend', '<li class="item">新项</li>');
container.insertAdjacentText('beforeend', '纯文本,不会解析成 HTML');
9.5 内存与性能问题
-
每次修改 DOM 都可能触发重排(reflow)和重绘(repaint)。最佳实践:
- 批量 DOM 更新时先构建
DocumentFragment或字符串一次性插入。 - 对频繁更新的 UI 使用虚拟 DOM、模板或最小化 DOM 变更。
- 使用
requestAnimationFrame来调度视觉变化,避免在短时间内多次强制布局。 - 当需要创建大量节点时,
Range.createContextualFragment()或document.createDocumentFragment()比多次appendChild更高效。
- 批量 DOM 更新时先构建
示例(DocumentFragment):
ini
const frag = document.createDocumentFragment();
for (let i=0;i<1000;i++) {
const li = document.createElement('li');
li.textContent = `项 ${i}`;
frag.appendChild(li);
}
ul.appendChild(frag); // 一次性插入,减少重排
9.6 跨站脚本(XSS)
- 直接将用户输入插入
innerHTML/insertAdjacentHTML风险极高。避免将未消毒的字符串作为 HTML 插入。 - 方案:对输入进行白名单过滤或在服务器端清洗;在前端使用文本节点(
textContent/createTextNode)插入纯文本;或使用成熟的库(如 DOMPurify)对 HTML 做消毒再插入。 - 不要用正则简单替换
<script>等做 XSS 过滤------很容易绕过。
10. scrollIntoView 方法
- 简单调用:
el.scrollIntoView()(旧签名)会滚动最近的可滚动祖先和视口,使元素可见。 - 现代签名提供选项对象:
el.scrollIntoView({ behavior: 'auto'|'smooth', block: 'start'|'center'|'end'|'nearest', inline: 'nearest'|'start'|'center'|'end' })。 - 常见用法:
php
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
- 若你只想在元素不可见时滚动,可先检查是否可见(用
getBoundingClientRect()或IntersectionObserver),再调用scrollIntoView。否则会造成不必要的跳转/滚动抖动。
11. 专有扩展(历史与兼容性)
-
历史上不同浏览器曾引入专有 API 或前缀方法,例如:
element.attachEvent(旧 IE) vsaddEventListenerwebkitMatchesSelector/msMatchesSelector→ 现在都可用matchesouterText(IE)为非标准属性(一些浏览器实现)scrollIntoViewIfNeeded(WebKit/Blink 的非标准扩展)
-
建议:尽量使用标准 API 并在必要时提供逐级兼容处理(feature-detect 或 polyfill)。
12. children 属性(补充说明)
(已在遍历部分提到)
要点回顾:
children只返回元素节点(Element),是HTMLCollection(live)。- 如果要静态快照用于修改,请转换为数组
Array.from(el.children)避免 live 集合带来的迭代问题。
13. contains / 常量(可能为你说的 "contants方法" 的两重含义)
Node.contains
parent.contains(node)返回true当且仅当node是parent的后代 或等于 parent 自身(注意:包含自身时也返回 true)。
javascript
console.log(document.body.contains(someNode));
console.log(elem.contains(elem)); // true
节点类型常量(Node.*)
Node.ELEMENT_NODE(1)、Node.TEXT_NODE(3)、Node.COMMENT_NODE(8)等常量用于判断node.nodeType。比硬编码数字更可读:
ini
if (node.nodeType === Node.ELEMENT_NODE) { /* 是元素 */ }
文档顺序比较
nodeA.compareDocumentPosition(nodeB)返回位掩码,可以检测包含关系、前后位置等(高级用法)。常用于实现稳定排序或比较节点文档顺序。
14. 插入标记(innerText / outerText)
-
textContentvsinnerText:textContent:返回/设置元素及子孙的文本内容(不考虑 CSS),性能快,不触发布局计算。innerText:返回"渲染"出来的文本,受 CSS(display:none、::before/::after)影响;读取innerText会触发重排以计算可见文本,较慢。innerText源于旧 IE,现已被广泛实现。
-
outerText:非标准属性(在某些浏览器可用)。赋值通常会替换元素自身为文本。
推荐:若只需要取/设置纯文本,用 textContent;若需要按可视文本(考虑 CSS)用 innerText 并注意性能开销。
15. 滚动(scrollIntoViewIfNeeded 与更深的思考)
-
scrollIntoViewIfNeeded:这是一个 非标准 的 API(曾在 WebKit/Blink 中存在),用于"仅当元素不可见时"滚动到视口中心或靠近边缘。不可依赖于所有平台/浏览器。 -
建议替代方案:
- 使用
IntersectionObserver检测元素是否可见;在不可见时调用scrollIntoView({ behavior:'smooth', block:'center' })。IntersectionObserver更高效、不会强制布局。 - 直接用
getBoundingClientRect()与视口尺寸做检测(简单但要谨慎,可能触发布局读取)。 - 对于复杂场景(内嵌滚动容器),计算容器的滚动位置并设置
scrollTop/scrollLeft,或使用Element.scrollTo({ top, behavior })。
- 使用
示例(IntersectionObserver + 自动滚动):
ini
const obs = new IntersectionObserver((entries) => {
entries.forEach(e => {
if (!e.isIntersecting) {
e.target.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
});
}, { threshold: 0.9 }); // 0.9 表示至少 90% 可见
obs.observe(targetElement);
深层思考要点:
-
自动滚动行为会影响用户体验(打断上下文、改变阅读位置),因此:
- 优先使用"只在必要时滚动"的策略(检测可见性)。
- 在可访问性场景(焦点移动)保证视觉跟随焦点,但考虑
preventScroll选项或平滑滚动以降低突兀感。
-
对于虚拟列表/长列表(如 infinite scroll),应结合 "可见性检测 + 按需渲染 + 回收节点" 的策略,而非简单地滚动到某处插入大量 DOM。
额外实战建议(速览)
- 避免频繁
innerHTML造成的重排与事件丢失;使用DocumentFragment、insertAdjacentHTML、或构造元素然后一次性 append。 - 为大量动态元素使用事件委托(在容器上绑定一个 listener),减少内存和绑定开销。
- 操作 NodeList / HTMLCollection 时注意 live vs static:若中间修改 DOM,先 snapshot
Array.from(...)。 - XSS:绝不要把用户输入直接通过
innerHTML插入。使用textContent或可信的 sanitizer。 - 测试各种浏览器的边界行为(尤其在面对旧浏览器或嵌入式浏览器时)。