- 文档/教程
- 前端
【免费下载链接】en.javascript.info
Modern JavaScript Tutorial
本篇文章基于 Modern JavaScript Tutorial(en.javascript.info)的「DOM 修改」章节中一个高价值实战任务(8-tree-count),完整还原问题场景与官方解法:给定一棵由嵌套ul/li构成的树,编写代码在每个<li>末尾追加其后代<li>的数量(叶子节点跳过)。读完本文你将掌握getElementsByTagName的集合语义、firstChild.data直接修改文本节点的技巧,以及如何用最简单的 DOM API 完成树形统计。
任务目标:为树的每个节点标注后代数量
任务要求十分明确:
- 页面中存在一棵由嵌套的
ul/li组织起来的树(source.view/index.html); - 为每个
<li>追加文本,内容为它的后代<li>的数量; - 跳过叶子节点(即没有子
<li>的节点)。
期望效果如下(solution.view/index.html 展示了运行结果):
Animals(含Mammals、Other两个分支及其全部子项)Mammals(含 4 个子项:Cows、Donkeys、Dogs、Tigers)Fishes(含Aquarium、Sea两个分支)- ……以此类推,最底层的
Cows、Donkeys等叶子节点不追加任何内容。
初始 HTML 结构
source.view/index.html 给出了任务的初始骨架,树形结构如下(已省略重复的Sea分支细节):
<!DOCTYPE HTML> <html> <body> <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> <script> // ... your code ... </script> </body> </html>注意这里的一个结构细节:每个非叶子<li>的第一个子节点是文本节点(如Animals、Mammals),随后才是子<ul>。这个文本节点的存在正是解法能直接“原地追加文本”的关键。
官方解法:两行核心逻辑
官方解法 给出的提示是:“要往每个<li>追加文本,我们可以直接修改文本节点data”。完整实现位于 solution.view/index.html:
let lis = document.getElementsByTagName('li'); for (let li of lis) { // 统计当前 <li> 下方的所有 <li> let descendantsCount = li.getElementsByTagName('li').length; if (!descendantsCount) continue; // 直接追加到文本节点(在文本后面拼上 [数量]) li.firstChild.data += ' [' + descendantsCount + ']'; }运行后页面呈现为:
Animals [5](其下共有 5 个<li>)Mammals [4](Cows、Donkeys、Dogs、Tigers)Other [3](Snakes、Birds、Lizards)Fishes [4](其下共有 4 个<li>)Aquarium [2](Guppy、Angelfish)Sea [1](Sea trout)- 最底层的
Cows、Donkeys等叶子节点原样不动。
关键 API 拆解
1.document.getElementsByTagName('li'):整棵树的所有<li>
getElementsByTagName是 DOM 搜索 API 中的经典方法,在教程的「搜索元素」章节中有明确说明:
elem.getElementsByTagName(tag)返回给定标签的元素集合;tag参数还可以传"*"表示“任意标签”;- 方法名中带有复数形式的
Elements,因为返回的是一个集合(HTMLCollection),这与返回单个元素的getElementById形成对照——这也是教程特别提醒过的易错点:getElementById没有字母"s",而getElementsByTagName有; - 集合中的元素可以通过索引访问,例如
document.getElementsByTagName('input')[0],也可以通过for...of直接迭代。
在第一行代码中,document.getElementsByTagName('li')收集的是文档范围内全部<li>,因此for...of循环会覆盖外层节点和所有深层嵌套节点。
2.li.getElementsByTagName('li'):子树内的后代<li>
解法最关键的一步是在元素实例上再次调用getElementsByTagName('li'):它不再搜索整个文档,而是只搜索以当前<li>为根的子树,返回其中所有的后代<li>(不包含当前节点自身)。因此descendantsCount恰好就是“当前节点的后代数量”。
由于嵌套的<li>只会以“祖先后代”的方式存在(树中不存在兄弟<li>互相包含的情况),这里无需递归、无需去重——每个后代<li>都唯一地归属于某个祖先分支,数量统计天然正确。
3.li.firstChild.data += ' [...]':直接操作文本节点
这是官方解法强调的“锦上添花”技巧。在 DOM 中,<li>Animals<ul>...</ul></li>这样的写法,其子节点依次为:
- 文本节点
"Animals"(即li.firstChild); - 元素节点
<ul>(即li.lastChild)。
文本节点的内容存储在.data属性中。所以li.firstChild.data += ' [5]'等价于“把' [5]'追加到文本末尾”,不会破坏原有文本、也不会误删子<ul>。整段操作没有新建任何元素、没有插入节点,仅仅是修改既有文本节点,因此即使getElementsByTagName返回的是实时(live)集合,循环遍历的集合结构也不会因修改而发生变化。
// 以 <li>Animals ...</li> 为例: li.firstChild // 文本节点 "Animals" li.firstChild.data // "Animals" li.firstChild.data += ' [5]'; // 变为 "Animals [5]"叶子节点为何被跳过
if (!descendantsCount) continue;叶子节点(如Cows、Guppy)内部没有任何<li>,li.getElementsByTagName('li').length返回0,条件!0为真,直接continue跳过。这一行同时承担了两件事:既过滤了叶子节点,又保证了文本格式的统一(只有有后代的节点才追加[N])。
完整可运行代码
将以下脚本放入 source.view/index.html 的<script>标签中,即可在浏览器中直接验证(最终效果见 solution.view/index.html):
<script> let lis = document.getElementsByTagName('li'); for (let li of lis) { let descendantsCount = li.getElementsByTagName('li').length; if (!descendantsCount) continue; li.firstChild.data += ' [' + descendantsCount + ']'; } </script>解法背后的 DOM 修改知识体系
本任务出自教程的「修改文档(Modifying the document)」章节,该章节系统讲解了 DOM 的创建、插入、删除与克隆方法,与本任务相关的核心知识点包括:
- 节点创建:
document.createElement(tag)创建元素节点、document.createTextNode(text)创建文本节点(本任务没有新建节点,走的是“改”而非“建”的路线); - 插入方法:
node.append(...)、node.prepend(...)、node.before(...)、node.after(...)、node.replaceWith(...)——如果任务要求的是“追加一个带计数的子元素”而非“在文本后拼接”,这些方法就是首选; - 文本插入安全性:
append等方法插入字符串时按纯文本处理(自动转义<、>),不会解析为 HTML;如需按 HTML 解析则用elem.insertAdjacentHTML(where, html); - 节点删除:
node.remove()。
理解这些背景有助于判断:本题选择直接修改firstChild.data,是因为只需要改文本、不涉及结构变更,这是最高效、最不容易出错的做法;而一旦需要新增元素节点(例如在每个<li>里再插入一个计数徽章),则应改用createElement+append的组合。
常见实现误区
- 误用
innerHTML整体重写:如li.innerHTML = li.innerHTML + ' [5]'会把子<ul>一并覆盖或导致结构损坏,且会触发不必要的重解析,务必避免; - 统计范围错误:只对顶层
<li>调用getElementsByTagName('li')无法得到每个节点的独立计数;必须在每一个<li>自身上调用,才能获得其子树范围; - 未跳过叶子节点:漏写
if (!descendantsCount) continue;会导致所有叶子<li>被追加成[0],与题目要求不符; - 误改元素节点:
firstChild取到的是文本节点才能用.data;若某个<li>以子元素开头(如<li><span>...</span></li>),则需要改用li.firstElementChild或先定位到对应文本节点。
小结
| 知识点 | 本任务中的应用 |
|---|---|
document.getElementsByTagName('li') | 获取文档内全部<li>,用于外层遍历 |
li.getElementsByTagName('li') | 获取当前节点子树内的后代<li>,用于计数 |
length属性 | 集合元素个数,即后代数量;为0说明是叶子节点 |
firstChild.data += ... | 向既有文本节点追加文本,保持结构不变 |
核心代码仅 4 行,却完整覆盖了“集合搜索 + 子树统计 + 文本节点修改 + 条件跳过”四个要点,是理解 DOM 搜索 API 与节点修改的最佳入门实战。完整题目与解法可在仓库中直接查看:任务描述、官方解法说明、可运行示例。
- 文档/教程
- 前端
【免费下载链接】en.javascript.info
Modern JavaScript Tutorial
相关推荐
现代 JavaScript 教程:遍历嵌套 ul/li 树,统计每个节点的后代数量
现代 JavaScript 教程:遍历嵌套 ul/li 树,统计每个节点的后代数量 导读 本文围绕《Modern JavaScript Tutorial》仓库中
文档/教程前端Modern JavaScript Tutorial 实战:遍历嵌套 `ul/li` 树,统计每个节点的文本标题与后代数量
Modern JavaScript Tutorial 实战:遍历嵌套 ul/li 树,统计每个节点的文本标题与后代数量 本文围绕 en.javascript.i
文档/教程前端用 JavaScript 从嵌套对象递归构建 ul/li 树形列表:innerHTML 与 DOM 双方案实战
用 JavaScript 从嵌套对象递归构建 ul/li 树形列表:innerHTML 与 DOM 双方案实战 导读 在《现代 JavaScript 教程》的"
文档/教程前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考