先记住这个答案
DOM 里的 Node 是抽象基接口,Document、Element、Text、Comment、DocumentFragment 等都继承自它,因此不存在一个“纯粹的 Node 对象”。Element 是 Node 的子接口,nodeType 为 1,只代表标签元素。Node 上定义了树结构通用能力,如 childNodes、parentNode、appendChild、textContent;Element 在此之上扩展了元素专属能力,如 id、className、getAttribute、querySelector、children。所以所有 Element 都是 Node,但文本节点、注释节点等 Node 不是 Element。
- Node 是抽象基接口,Element 是其子接口
- childNodes 含全部节点,children 只含元素
- 文本注释是 Node 但不是 Element
继承层次决定 API 归属
Node 接口继承自 EventTarget,它是抽象类,页面上不会出现一个直接实例化的 Node。它承载的是“树中一员”的通用语义:parentNode、childNodes、firstChild、nextSibling、isConnected,以及 appendChild、insertBefore、removeChild、cloneNode、contains 等结构操作方法。nodeType 属性返回数字常量,ELEMENT_NODE 是 1、TEXT_NODE 是 3、COMMENT_NODE 是 8、DOCUMENT_NODE 是 9,这是区分节点种类的根本依据。
Element 在 Node 之上叠加了元素语义:tagName、id、className、classList、attributes、getAttribute、setAttribute、querySelector、matches、children 等。这些 API 只对标签有意义,所以不挂在 Node 上。同样地,文本节点的数据操作 data、splitText 挂在 CharacterData 上而不是 Node 上。浏览器这样分层,是为了让每个接口只承诺自己能兑现的能力,例如向文本节点 appendChild 会抛异常,因为它在结构上不能有子节点。
遍历子节点时过滤出真实元素
假设要统计一个评论列表容器里的评论条数,HTML 由模板渲染,子节点之间混有换行文本和开发者留下的注释。如果用 container.childNodes.length,结果是包含空白文本节点和注释的总数,明显偏大。正确决策是改用 container.children.length,它返回 HTMLCollection,只包含 Element 子节点;或者遍历 childNodes 时用 node.nodeType === 1 过滤。这样得到的就是真实评论元素数。
之所以这样处理,是因为 HTML 源码里的换行和缩进会被解析成文本节点,它们在 DOM 树里占据位置但不对应任何标签。childNodes 的设计目标是完整反映树的结构,包括文本与注释,因此它是 NodeList;children 则是 Element 视角的快捷过滤结果。反过来,如果业务需要读取或修改元素之间的文字,就必须回到 childNodes,因为 children 会把文本节点整个隐藏掉。
按 Element 假设处理 Node 时容易失效的条件
常见失效点是拿到一个类型标注为 Node 的值就直接调用元素方法。例如遍历 childNodes 时对每项调用 getAttribute 或 classList,一旦遇到文本节点就会抛 TypeError,因为这些方法不存在于 Node 上。另一个边界是 parentNode 与 parentElement 的差异:当父级是 Document 时,parentNode 有值而 parentElement 返回 null,按元素处理会漏判。还有 nextSibling 可能返回文本节点,而 nextElementSibling 保证返回元素或 null。
对应的防御手段是先用 nodeType 或 instanceof Element 收窄类型,再访问元素专属 API;TypeScript 项目中可以用类型谓词封装 isElement 函数。代价是多一层判断和略冗长的代码,但换来的是对注释、文本、文档节点混在树中时的健壮性。需要注意 instanceof 在跨 iframe 场景会失败,因为不同窗口有各自的 Element 构造函数,此时用 nodeType === 1 更可靠,这是跨文档判断的边界。
容易答错的地方
- 认为 Node 就是元素节点
- Node 是抽象基接口,文本、注释、文档、属性节点都是 Node 但不是元素。把
childNodes的结果当作元素集合直接读id会出错,必须先用nodeType === 1过滤或改用children。 - 认为 children 和 childNodes 只是名字不同
children只含元素节点,childNodes含元素、文本、注释全部子节点。HTML 中的换行缩进会产生文本节点,二者长度常常不等,混用会导致统计或下标定位错误。
面试官还会怎么问?
文本节点能用 textContent 之外的什么属性读写内容?
文本节点继承自 CharacterData,可用 data 或 nodeValue 读写文本内容。对元素节点而言 nodeValue 是 null,这也是 Node 基类属性在部分子类型上不适用的典型例子。
querySelector 返回的可能是非元素节点吗?
不会。querySelector 用 CSS 选择器匹配,只可能命中 Element,找不到时返回 null。而 getRootNode、parentNode 这类 Node 层 API 则可能返回 Document 或文本等非元素节点。
为什么向文本节点 appendChild 会抛异常?
Node 基类声明了 appendChild,但不是所有节点类型都能有子节点。文本、注释这类 CharacterData 节点没有容纳子节点的结构,调用时会抛出 DOM 异常,这是基类能力在子接口上不适用的边界。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。