现代 JavaScript 教程:遍历嵌套 ul/li 树,统计每个节点的后代数量

发布时间:2026/10/6 18:45:57
现代 JavaScript 教程:遍历嵌套 ul/li 树,统计每个节点的后代数量 文档/教程前端【免费下载链接】en.javascript.infoModern JavaScript Tutorial项目地址https://gitcode.com/gh_mirrors/en/en.javascript.info点击查看免费下载导读本文围绕《Modern JavaScript Tutorial》仓库中Count descendants统计后代数量这一经典练习题展开讲解如何用少量原生 DOM API 遍历任意深度的嵌套ul/li列表既能取出每个li自身的标题文本不含其子树内容又能统计其下嵌套的所有li后代总数。读完本文你将掌握querySelectorAll、firstChild.data、getElementsByTagName的组合用法并理解文本节点data属性与 DOM 遍历的核心原理可直接复用于目录树、菜单折叠、分类计数等常见前端场景。任务背景要解决什么问题在仓库的 任务描述文件 中题目给出了一棵由多层嵌套ul/li构成的树形结构要求编写代码对每一个li输出它内部的文本仅自身标题不含子树的文字它嵌套的后代li数量包含任意深度的所有后代。初始 HTML 结构与仓库 source.view/index.html 一致是一个三层嵌套的动物分类树例如ul liAnimals ul liMammals ul liCows/li liDonkeys/li liDogs/li liTigers/li /ul /li liOther ul liSnakes/li liBirds/li liLizards/li /ul /li /ul /li liFishes !-- 同样嵌套 Aquarium / Sea 等分支 -- /li /ul题目的难点在于标题文本与子树文本混在同一个li的 DOM 子节点中且树的深度不固定必须用通用方法而非硬编码层级。官方解答三行核心代码解答文档solution.md给出的思路非常精炼完整可运行版本见 solution.view/index.htmlfor (let li of document.querySelectorAll(li)) { // 1. 从第一个子节点文本节点读取标题 let title li.firstChild.data; title title.trim(); // 去掉首尾多余空白 // 2. 统计后代 li 数量含任意深度的嵌套 let count li.getElementsByTagName(li).length; alert(title : count); }运行结果依次为Animals: 7Mammals: 4Cows: 0Donkeys: 0Dogs: 0Tigers: 0Other: 3Snakes: 0、Birds: 0、Lizards: 0Fishes: 3Aquarium: 2、Guppy: 0、Angelfish: 0Sea: 1、Sea trout: 0叶子节点如Cows没有后代计数为 0Animals直接与间接后代共 7 个。第一步querySelectorAll(li)收集全部节点for (let li of document.querySelectorAll(li)) {document.querySelectorAll(li)返回文档中所有li元素的静态 NodeList包括任意嵌套深度的后代结果可以直接用for...of遍历。这正是解决深度不固定问题的关键——不需要递归自己实现遍历选择器天然支持任意层级。与getElementsByTagName不同querySelectorAll返回的是静态集合在遍历期间即使 DOM 被修改集合内容也不会变化遍历是安全的。而getElementsByTagName(li)返回动态集合会实时反映 DOM 变化这在下文统计后代数量时会带来微妙的差异。第二步firstChild.data读取标题文本let title li.firstChild.data;这是本解法的精华所在。对于一个形如下面的liliAnimals ul.../ul /li其 DOM 子节点序列为文本节点Animals\n→ 元素节点ul。也就是说第一个子节点恰好就是保存标题文字的文本节点因此li.firstChild拿到的就是文本节点而.data取出其中的字符串内容。关于data属性本仓库的配套章节 05-basic-dom-node-properties/article.md 有专门讲解data与nodeValue几乎等价仅存在极细微的规范差异日常使用更推荐更短的datainnerHTML只对元素节点有效而文本节点、注释节点等非元素节点要用data/nodeValue读取内容data既可以读也可以写是可修改属性。同样的技巧在仓库另一道题 3-tag-in-comment/solution.md 中也出现过body.firstChild.data用于读取body后紧跟注释节点时的文本内容。可见第一个子节点 data是从 DOM 树中取裸文本的通用手段。另外解答中的title.trim()很重要HTML 源码中liAnimals换行后才有ul浏览器解析时文本节点会带上换行与缩进空白如Animals\n。trim()去掉首尾空白后标题才是干净的Animals。若不处理alert输出会出现多余空行。边界firstChild不是文本节点时怎么办如果li写成lia href#Animals/a .../li那么第一个子节点是a元素而非文本节点firstChild.data就会得到undefined。此时应根据结构改用li.querySelector(:scope a).textContent—— 取直接子链接的文字或li.firstElementChild.textContent—— 取第一个元素子节点或li.childNodes[0]并先通过nodeType 3判断其确为文本节点。nodeType的数值约定在 article.md 中有明确说明元素节点为1、文本节点为3、document 为9。在结构不固定的场景先检查li.firstChild.nodeType再取data是更稳妥的写法。第三步getElementsByTagName(li).length统计后代let count li.getElementsByTagName(li).length;li.getElementsByTagName(li)以当前li为根在其后代不含自身中查找所有li返回一个动态 HTMLCollection取.length即得到后代总数。由于它天然递归搜索所有层级所以Animals一类的高层节点能一次性数出全部间接后代无需自己写递归函数。注意这里不能写成li.querySelectorAll(li)的等价替代吗可以两者都能数出后代数量但有一个细节差异getElementsByTagName返回动态集合querySelectorAll返回静态集合。在本例中我们只是立刻读取.length两种写法结果一致但若在读取长度之前修改了 DOM比如边遍历边删除节点动态集合的.length会实时变化可能造成计数与直觉不符。因此读取后再操作是安全习惯。一个容易踩的坑为什么标题里不含子树文字有读者可能会想li.textContent不也能拿到文字吗是的但它会连同所有后代文本一起拼接。例如对Animals这个litextContent会返回Animals\nMammals\nCows\nDonkeys...一长串根本无法分离出仅自身标题。这正是本解法选择firstChild.data的原因它只读第一个文本节点天然把标题与子树隔离。对照 article.md 中textContent的定义——元素内部的所有文本去掉所有标签但保留文本——textContent适合取整块纯文本data适合取单个文本节点的内容两者分工不同。扩展实战把计数直接写进 DOM仓库中有一道进阶练习题 8-tree-count/solution.view/index.html逻辑几乎同源但输出方式从alert变成了直接改写 DOM让计数即时显示在树中let lis document.getElementsByTagName(li); for (let li of lis) { let descendantsCount li.getElementsByTagName(li).length; if (descendantsCount ! 0) { li.firstChild.data [ descendantsCount ]; } }这里展示了两个延伸点字符串拼接写回li.firstChild.data [...]直接修改文本节点内容等价于data data [...]是安全且高效的文本更新方式不会像innerHTML那样整段重写并触发资源重载。动态集合的连带效应外层用getElementsByTagName动态当内层循环修改了li的文本后由于修改的是文本节点而非元素结构集合本身不受影响遍历仍正确。这也反过来印证了动态/静态集合差异的实际影响范围。完整可运行示例将 source.view/index.html 中的script// ... your code.../script替换为下面代码即可在浏览器中直接验证script for (let li of document.querySelectorAll(li)) { let title li.firstChild.data.trim(); let count li.getElementsByTagName(li).length; alert(title : count); } /script也可以参考 solution.view/index.html 的完整成品进行对照调试。小结这道题教给我们的四件事选择器天然支持任意层级querySelectorAll(li)/getElementsByTagName(li)都是递归搜索处理不定深度的树无需手写递归。文本节点用data读firstChild.data是提取节点自身裸文本的标准手法配合trim()处理空白前提是确认第一个子节点确实是文本节点可用nodeType 3校验。textContent与data分工不同前者取整块纯文本含后代后者取单个文本节点内容选错会导致标题与子树文字混淆。动态集合与静态集合的差异getElementsByTagName动态、querySelectorAll静态只读取length时两者等效但边修改 DOM 边遍历时行为不同需要根据场景选择。这套遍历 文本提取 后代计数的组合拳是后续实现折叠菜单、面包屑、目录自动编号、树形组件计数等功能的基础值得反复练习直到形成条件反射。延伸阅读本章完整知识体系见 05-basic-dom-node-properties/article.mdnodeType、tagName/nodeName、innerHTML、outerHTML、data、textContent、hidden等节点属性全览同组练习 2-lastchild-nodetype-inline/solution.md 揭示了脚本执行时最后一个节点是script本身的解析时序陷阱值得一并阅读。赞分享文档/教程前端【免费下载链接】en.javascript.infoModern JavaScript Tutorial项目地址https://gitcode.com/gh_mirrors/en/en.javascript.info点击查看免费下载相关推荐Brackets 单元测试指南基于 Jasmine 的 SpecRunner 测试体系与用例编写实践Brackets 单元测试指南基于 Jasmine 的 SpecRunner 测试体系与用例编写实践 本文以 test/README.md https://l文档/教程前端现代JavaScript迭代器遍历数据结构的终极指南现代JavaScript迭代器遍历数据结构的终极指南 现代JavaScript迭代器是遍历数据结构的强大工具它提供了一种统一的方式来访问集合中的元素无论集MaLiang API完全指南掌握iOS Metal绘图的核心类与最佳实践MaLiang API完全指南掌握iOS Metal绘图的核心类与最佳实践 MaLiang是一款基于Metal的iOS绘图库为开发者提供了高效、灵活的涂鸦和上一篇别再凭感觉找线NetBox 网络真相源快速上手下一篇Upscayl AI 图像放大三步拿到第一张高清图创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考