☰
现代 JavaScript 教程实战:用 DOM 遍历统计嵌套列表的后代数量(tree-info 任务解析)
2026/10/9 7:35:22 网站建设 项目流程
  • 文档
  • 教程
  • 前端

【免费下载链接】zh.javascript.info

现代 JavaScript 教程(The Modern JavaScript Tutorial),以最新的 ECMAScript 规范为基准,通过简单但足够详细的内容,为你讲解从基础到高阶的 JavaScript 相关知识。

项目地址:https://gitcode.com/gh_mirrors/zh/zh.javascript.info
点击查看免费下载

本篇技术指南基于 zh.javascript.info 仓库中《节点属性:type,tag 和 content》章节的实战练习任务「计数后代」(2-tree-info/task.md),以一棵嵌套的ul/li树为场景,完整讲解如何遍历 DOM 节点、读取文本节点内容、统计元素后代数量,并深入剖析querySelectorAll、firstChild.data、getElementsByTagName等核心 API 的底层机制与适用边界。读完本文,你将能够独立完成"为每个树节点显示标题与子孙数量"这类典型的 DOM 操作需求,并理解 DOM 节点类层次(EventTarget → Node → Element → HTMLElement)对属性与方法的决定性影响。

任务背景:一棵真实的嵌套列表树

任务给出了一个典型的树状导航结构(完整 HTML 见 source.view/index.html):

<ul> <li>Animals <ul> <li>Mammals <ul> <li>Cows</li> <li>Donkeys</li> <li>Dogs</li> <li>Tigers</li> </ul> </li> <li>Other <ul> <li>Snakes</li> <li>Birds</li> <li>Lizards</li> </ul> </li> </ul> </li> <li>Fishes <ul> <li>Aquarium <ul> <li>Guppy</li> <li>Angelfish</li> </ul> </li> <li>Sea <ul> <li>Sea trout</li> </ul> </li> </ul> </li> </ul>

任务要求对每一个<li>元素分别输出两件事:

  1. 它自身的文本内容(不含子树中的文本,例如Animals而不是Animals Mammals Cows ...);
  2. 它嵌套的后代<li>数量,这里的"后代"指所有层级的深层后代,而非仅直接子级。

例如根级别的Animals项应显示Animals: 2(它下面直接挂Mammals与Other两个 li),而Mammals应显示Mammals: 4(Cows、Donkeys、Dogs、Tigers)。

三步解法:从选择节点到读取文本再到统计后代

官方参考解法(完整可运行版本见 solution.view/index.html)由三个相互衔接的步骤构成。

第一步:用querySelectorAll选中全部<li>

for (let li of document.querySelectorAll('li')) { // ... }

querySelectorAll由Element类提供,返回的是符合 CSS 选择器的全部匹配元素组成的集合(NodeList),天然支持for...of迭代。它执行的是一次全文档范围的元素级搜索——这正是Element类"提供元素级导航与搜索方法(getElementsByTagName、querySelector)"这一能力的具体体现(见 article.md 中对 DOM 节点类的说明)。

由于querySelectorAll的匹配范围是"所有后代",所以无论树嵌套多深,每一个li都会被选中且只被选中一次,这正好满足"为每个<li>显示"的需求。

第二步:用firstChild.data读取标题文本

每个<li>的内部结构是这样的:先是文本节点(如Animals),然后是子元素节点(嵌套的<ul>)。因此标题文本恰是li的第一个子节点——一个Text类型的节点:

for (let li of document.querySelectorAll('li')) { let title = li.firstChild.data; // title 是 <li> 中任何其他节点之前的文本 }

这里用到了两个关键知识点:

  • firstChild:由Node类提供的属性,返回节点的第一个子节点(不区分节点类型,可能是元素、文本或注释)。
  • data(即nodeValue):CharacterData抽象类的属性,用于读写文本节点、注释节点等非元素节点的内容。innerHTML只对元素节点有效,而文本节点对应的是nodeValue和data两个几乎等价的属性,官方文档明确建议"使用data,因为它更短"。

官方解法在读取后还做了收尾处理:title = title.trim();,去除文本节点两侧可能存在的换行与空格(原始 HTML 中<li>与文本之间可能存在格式化空白)。

第三步:用getElementsByTagName('li')统计后代

let count = li.getElementsByTagName('li').length; alert(title + ': ' + count);

getElementsByTagName同样是Element类提供的方法,与querySelectorAll的关键差异在于:它只在当前元素的后代范围内搜索,因此li.getElementsByTagName('li')返回的正是当前<li>内部所有层级的嵌套<li>,length即为后代总数。这正是任务所要求的"所有后代,包括深层嵌套的后代"——getElementsByTagName的搜索天然是递归的,无需自己编写递归函数。

将三步组合,就是完整解法:

for (let li of document.querySelectorAll('li')) { // 从第一个子节点(文本节点)读取标题 let title = li.firstChild.data; title = title.trim(); // 去掉首尾多余空白 // 统计该 li 内部嵌套的所有后代 li let count = li.getElementsByTagName('li').length; alert(title + ': ' + count); }

运行后依次弹出:Animals: 2、Mammals: 4、Other: 3、Cows: 0、Donkeys: 0、Dogs: 0、Tigers: 0、Fishes: 2、Aquarium: 2、Guppy: 0、Angelfish: 0、Sea: 1、Sea trout: 0。

为什么会踩坑:三个必须理解的 DOM 细节

这个任务之所以被评为 importance 5 的高价值练习,是因为它精准地检验了三个容易出错的概念。

细节一:<li>的第一个子节点是文本节点,而不是<ul>

如果把li.firstChild误认为是嵌套的<ul>,就会读不到标题。firstChild不区分节点类型,对于"<li>Animals<ul>..."这样的结构,子节点顺序是:文本节点Animals→ 元素节点ul。要跳过文本直接拿元素,应使用firstElementChild(Element类提供,只返回元素节点);而要拿文本内容,则正好用firstChild.data。

细节二:nodeType是判断节点类型的"硬核"手段

article.md明确指出,nodeType是查看节点类型的传统数值属性:1表示元素节点,3表示文本节点,9表示 document 对象,且只读。在本任务中可以这样验证li的第一个子节点确实是文本节点:

alert(li.firstChild.nodeType); // 3 => 文本节点

细节三:getElementsByTagName返回"活"集合(live collection)

与querySelectorAll返回静态快照不同,getElementsByTagName返回的是动态更新的HTMLCollection。在本任务中二者结果一致,但当你后续动态增删 DOM 节点时,HTMLCollection会实时反映变化,而NodeList不会。选择哪个 API 取决于你是否需要"活"引用。

从任务到实战:这些 DOM 属性的完整图谱

本任务是 article.md 中"节点属性:type、tag 和 content"知识点的集中演练场。该章总结的全部核心属性在此任务中都能找到用武之地:

属性任务中的角色性质
nodeType验证子节点类型(元素=1,文本=3,document=9)数值、只读
nodeName/tagName获取标签名(HTML 模式下恒为大写,如LI)只读,tagName仅元素可用
innerHTML元素内 HTML 内容,可读写可修改,插入的<script>不会执行
outerHTML元素完整 HTML;写入不会改元素本身,而是替换 DOM 位置可修改但有陷阱
nodeValue/data本任务读取标题文本的核心(firstChild.data)文本/注释节点内容,可读写
textContent元素内纯文本(去标签);写入时特殊字符按字面处理,可安全插入用户文本可读写
hidden等价于style="display:none"的简洁开关可读写

其中与本任务最相关的两组对应关系值得强调:

  • 元素节点读innerHTML,非元素节点读data:innerHTML只对元素有效,文本节点和注释节点必须通过nodeValue/data读写。任务中title = li.firstChild.data正是这种分工的典型用法。
  • 读文本用data,写文本用textContent:若任务改为"把用户输入的标题写入<li>",正确做法是li.textContent = userInput——它把所有符号按字面处理,不会把<b>之类的输入变成真实标签,是防 XSS 的安全写入方式。

同章节其他任务:巩固同样的概念

本练习所在的 05-basic-dom-node-properties 目录下还有两个姊妹练习,用更刁钻的角度巩固相同的知识:

  • nodeType 中是什么?:问<script>执行时document.body.lastChild.nodeType是多少。陷阱在于:脚本执行瞬间,浏览器尚未解析完页面其余部分,lastChild就是正在执行的<script>元素本身,答案是1(元素节点)——这提醒我们"DOM 是随着解析逐步构建的"。
  • 注释中的标签:body.innerHTML = "<!--" + body.tagName + "-->"之后body.firstChild.data是什么?答案是BODY——因为tagName在 HTML 模式下恒为大写,且data读取注释节点内容(<!--...-->内部文本)的能力同样适用于注释节点,不只是文本节点。

这两个任务与「计数后代」共同验证了同一批核心结论:tagName大写规则、firstChild与data的节点类型无关性、脚本执行时机的 DOM 状态。把它们放在一起练习,能一次性打通"节点类型 + 内容读取 + 遍历搜索"三条主线。

总结

「计数后代」这个练习虽然代码只有几行,却是 DOM 基础能力的"试金石":querySelectorAll负责全文档选择、firstChild负责定位文本子节点、data负责读取非元素节点内容、getElementsByTagName负责在子树内递归计数后代。理解每个 API 的类归属(Node提供树导航,Element提供元素搜索)和类型差异(元素 vs 文本 vs 注释),才是写出健壮 DOM 代码的前提。建议直接打开 solution.view/index.html 在浏览器中运行,观察每一步弹窗结果,再结合 article.md 的完整属性表做变体实验(例如把标题改为textContent写入、把计数改为querySelectorAll(':scope > ...')),就能彻底掌握树形 DOM 的遍历与统计技巧。

  • 文档
  • 教程
  • 前端

【免费下载链接】zh.javascript.info

现代 JavaScript 教程(The Modern JavaScript Tutorial),以最新的 ECMAScript 规范为基准,通过简单但足够详细的内容,为你讲解从基础到高阶的 JavaScript 相关知识。

项目地址:https://gitcode.com/gh_mirrors/zh/zh.javascript.info
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询