先记住这个答案
核心区别在于节点类型过滤:children 是 HTMLCollection,只包含 Element,适合列表操作;childNodes 是 NodeList,包含所有 Node。两者均为 live 集合,实时反映 DOM 变化,但遍历时要注意文本节点的影响。
- children 只含元素节点
- childNodes 包含文本和注释
- 两者都是实时集合
节点类型过滤与集合的实时性
children 被定义为 Element 上的只读属性,返回 HTMLCollection,其中每一项都是 Element 类型。浏览器在构造该集合时会自动跳过所有非元素节点,例如文本节点、注释和文档类型节点。这意味着即使父节点下存在换行产生的文本节点,children 的长度也不会因此改变。
childNodes 则定义在 Node 上,返回 NodeList,它包含所有类型的子节点,包括元素、文本(包括空白)、注释以及可能的 CDATA 等。当使用 for 循环等遍历时,必须检查节点类型,否则可能把文本节点当作元素处理。两者都是 live 集合:当子节点增删时,集合长度会实时变化。
遍历列表元素的场景:避免空白文本干扰
假设有一个 <ul id="list">,其中包含 <li> 元素,并且在标签间有缩进换行的文本节点。若用 childNodes 遍历,length 会是元素数量的两倍以上(每个元素前后都有文本节点),打印 nodeName 时会出现 #text,最终无法直接拿到想要的 li。此时必须判断 nodeType === 1 或使用 children。
实际开发中,对用户可控的 DOM 结构,空白文本可能不存在,但对格式化后的 HTML,空白文本很常见。采用 children 可以直接得到所有子元素,无需额外过滤。若确实需要访问文本节点,例如提取纯文本内容,则必须使用 childNodes,并结合 nodeValue 读取。
实时集合的副作用与索引陷阱
children 和 childNodes 都是实时集合,在遍历过程中如果动态删除或添加子节点,集合的大小会立即变化,这可能导致跳过某些元素或无限循环。例如使用 for (let i = 0; i < parent.childNodes.length; i++) 时删除当前文本节点,会使长度收缩,未遍历的节点索引前移。
另一个边界是 children 不能获取文本节点,所以当需要精确处理文本内容时(如拆分词句),只能依赖 childNodes。另外,为了避免 live 集合带来的索引失效,建议将集合快照为静态数组:Array.from(parent.children),但快照与 DOM 不再同步,要注意场合取舍。
容易答错的地方
- 认为两者都只包含元素节点
childNodes包含所有子节点类型,包括文本、注释;children才仅返回元素节点。MDN 文档对不同接口的说明明确区分,混淆会导致遍历时误判节点类型。- 认为两个集合是静态快照
children和childNodes都是 live 集合,会随 DOM 变化自动更新。若以为它们固定不变,在操作 DOM 后使用旧的集合长度会得出错误结论。
面试官还会怎么问?
children 和 childNodes 返回的集合类型有何不同?
children 返回 HTMLCollection,childNodes 返回 NodeList。HTMLCollection 有 namedItem 方法,而 NodeList 没有;两者都支持索引访问,但包含节点范围不同。
如何通过 childNodes 过滤出元素节点?
检查 node.nodeType === Node.ELEMENT_NODE,或者使用 Array.from(childNodes).filter(n => n.nodeType === 1)。更直接是使用 children,因为后者已经天然过滤。
遍历 childNodes 时如何避免把空白文本当节点?
使用 children 直接跳过;若需处理文本节点,可忽略所有 nodeType 为 TEXT_NODE 且 nodeValue 只含空白的节点,或使用 trim() 判断。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。