递归遍历dom需区分childnodes(含所有节点)和children(仅元素节点);避免queryselector嵌套调用;深递归用contains或栈模拟;修改dom时应先收集目标再批量操作。

递归遍历 DOM 时,node.childNodes 和 node.children 别混用
前者返回所有节点(含文本、注释),后者只返回元素节点。递归中若想跳过空白文本节点,用 children 更干净;但若需处理文本内容(比如提取纯文本),就必须用 childNodes 并手动过滤。
常见错误是直接遍历 childNodes 却没检查 node.nodeType === Node.ELEMENT_NODE,结果在文本节点上调用 querySelector 报错:TypeError: node.querySelector is not a function。
- 需要修改/查找子元素 → 优先用
node.children,省去类型判断 - 需要读取或替换文本内容 → 用
node.childNodes,再用if (node.nodeType === Node.TEXT_NODE)分支处理 - IE8 及更早版本不支持
children,若需兼容,得 fallback 到childNodes+ 类型过滤
querySelector 在递归里反复调用很慢,别这么干
每次调用 querySelector 都触发一次全子树匹配,嵌套递归中叠加调用会让时间复杂度接近 O(n²)。尤其在深层 DOM 或频繁修改后调用,卡顿明显。
正确做法是:把查找逻辑下沉到递归体内部,用属性、标签名、类名等原生属性直接判断,比如 node.tagName === 'INPUT' 或 node.classList.contains('dirty')。
- 要找所有
input[type="email"]→ 递归中检查node.tagName === 'INPUT' && node.type === 'email' - 要改某个 class 的所有后代 → 用
node.classList.contains('target'),而不是每层都node.querySelector('.target') - 如果必须用 CSS 选择器做复杂匹配(比如伪类、组合器),那说明递归不是最优解,该换
document.querySelectorAll一次性获取再遍历
递归太深容易爆栈,node.contains(target) 是个安全替代
DOM 树超过 10000 层(极端但存在,比如模板生成的无限嵌套)时,纯递归会触发 RangeError: Maximum call stack size exceeded。浏览器对 JS 调用栈深度有限制,且不可配置。
对“判断某节点是否在目标节点的子树中”这类需求,直接用原生 node.contains(target),它由引擎底层实现,无栈溢出风险,也比手写递归快一个数量级。
- 检查
el是否为root的后代 → 用root.contains(el),别写递归向上查parentNode - 要找出所有满足条件的后代节点 → 先
root.querySelectorAll('*'),再用Array.from(...).filter()筛选,避免递归 - 真需要深度遍历又怕栈溢出 → 改用栈模拟(
Array存待处理节点),但多数场景没必要,先测真实 DOM 深度再说
修改 DOM 同时遍历,顺序错乱是常态
边遍历 node.children 边调用 node.removeChild() 或 node.appendChild(),会导致索引偏移——因为 children 是实时集合(live collection),删一个,后面所有项前移一位,下标就对不上了。
典型现象:只删了偶数位置的子节点,或者遍历时漏掉紧邻被删节点的下一个节点。
- 要删除匹配节点 → 先收集所有目标节点(
const targets = []),遍历完再统一targets.forEach(n => n.remove()) - 要插入新节点到每个匹配位置 → 用
node.insertBefore(newNode, refNode),并确保refNode是遍历时的原始引用,别依赖下标 - 用
for (let i = node.children.length - 1; i >= 0; i--)倒序遍历可避免索引漂移,但语义不如收集后批量操作清晰
递归遍历 DOM 看似简单,真正麻烦的是边界条件:文本节点的干扰、实时集合的陷阱、栈深度的隐形限制,还有修改与遍历耦合时的竞态。这些地方不写测试很难暴露,一上线就在用户最深的嵌套页面里崩。










