先记住这个答案
NodeList 和 HTMLCollection 都是类数组的 DOM 节点集合。关键区别:NodeList 可包含任何节点类型(如文本节点),且有的静态(querySelectorAll 返回),有的动态(childNodes 返回);HTMLCollection 只包含元素节点,且是动态的(如 getElementsByTagName 返回)。另外,NodeList 有 forEach 方法,HTMLCollection 没有,但可以用 Array.from 或展开符转为数组后使用数组方法。
- HTMLCollection 只含元素节点且通常是动态的
- NodeList 可含文本节点,可能是静态或动态
- NodeList 有 forEach,HTMLCollection 没有
集合的组成与动态性差异
NodeList 由 childNodes 和 querySelectorAll 等 API 返回。childNodes 是动态的,集合会随 DOM 变化实时更新;querySelectorAll 返回静态快照,此后 DOM 变化不会影响它。NodeList 可以包含文本节点、注释节点和元素节点。
HTMLCollection 由 children、getElementsByTagName 等返回。HTMLCollection 是动态的(live),即 DOM 变化会反映在集合中。children 属性返回的 HTMLCollection 是动态的;getElementsByClassName 返回的也是动态 HTMLCollection。它们只包含元素节点,不含文本和注释。
遍历 li 并统计包含文本的项
假设要统计一个 ul 中所有 li 的个数,其中 li 内可能有文本或嵌套元素。如果使用 parent.children,它返回 HTMLCollection,只含 li 元素,但动态性可能导致遍历中意外增删。如果先获取快照,可用 Array.from。
更复杂:需要统计包含直接文本的 li。必须遍历所有子节点,包括文本节点,这时不能使用 children,而应使用 childNodes。childNodes 返回 NodeList(动态),包含元素、文本、注释。遍历时通过 nodeType 判断,且需注意动态性:如果在遍历中删除节点,索引会变化,应倒序遍历或用静态副本。决策:先转静态数组再处理,避免动态干扰。
静态与动态的失效条件
静态 NodeList(如 querySelectorAll)不随 DOM 变化,但也因此缓存了旧结果。如果先获取列表,再新增匹配元素,列表不会包含新项,容易产生陈旧数据。若需要始终最新,应每次查询或使用动态 HTMLCollection。
动态 HTMLCollection(如 children)实时反映删除,但遍历时可能导致跳过元素:假设循环 i 递增,删除当前元素会使集合长度减小,下一个元素前移,i++ 会跳过它。常见处理是倒序遍历或转换为数组。这两种方式都有成本,需根据操作频率选择。
容易答错的地方
- 认为 NodeList 总是静态的
- 实际上 childNodes 返回的 NodeList 是动态的,只有 querySelectorAll 返回静态 NodeList。许多开发者误以为所有 NodeList 都是静态快照,于是缓存 childNodes 后忽略其动态性,在遍历时可能因集合实时变化而跳过或重复处理节点。
- 混淆 HTMLCollection 和 NodeList 的包含范围
- HTMLCollection 仅包含元素,而 NodeList 包含所有节点类型。例如 childNodes 可能包含文本节点,而 children 就没有。若需要过滤文本,仅用 children 会漏掉文本内容,错误地以为元素就是全部。
面试官还会怎么问?
如何将 HTMLCollection 转换为数组?
可以使用 Array.from(htmlCollection) 或 [...htmlCollection],因为两者都是类数组且可迭代。转换后即可使用 map、filter 等数组方法,但失去了动态性。
querySelectorAll 和 getElementsByTagName 返回的集合有何区别?
querySelectorAll 返回静态 NodeList,只匹配选择器时的元素,不随 DOM 变化。getElementsByTagName 返回动态 HTMLCollection,实时反映新增或删除的匹配元素。后者因此不适合缓存后依赖快照的场景。
为什么 NodeList 有 forEach 而 HTMLCollection 没有?
JavaScript 引擎给 NodeList 提供了迭代器和 forEach 方法,因其设计更现代;HTMLCollection 历史更久,且不是可迭代对象(在某些旧环境)。但都可以通过 Array.from 转换后用数组方法。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。