
本文详解为何原递归函数无法处理 `
` 等非 `li` 元素,并提供基于 `for...of` 的健壮解决方案,确保所有非 `li` 元素(如 `
`、``、``)被安全展开为其子文本节点,最终输出符合预期的纯净 html 片段。
问题根源在于 NodeList.prototype.forEach() 的执行时序与 DOM 树动态变更的冲突。当递归调用 testFn(e) 后立即执行 e.replaceWith(...e.childNodes) 时,e 被从 DOM 中移除,其子节点被插入到原位置——这会修改当前 node.childNodes 的长度和索引顺序。而 forEach() 内部是基于初始快照遍历的,后续迭代仍按原始 NodeList 进行,导致部分节点被跳过(尤其是紧邻被替换节点之后的兄弟节点),造成
等元素未被处理。
使用 for...of 循环可规避该问题,因为它在每次迭代时都重新获取当前 childNodes 的迭代器,能响应实时 DOM 变化;更重要的是,它使控制流更清晰、避免闭包陷阱,便于逻辑调试。
以下是修正后的完整实现:
let html = `
<ol>
<li><a href="https://www.php.cn/link/93ac0c50dd620dc7b88e5fe05c70e15b">foo link text</a>;</li>
<li><a href="https://www.php.cn/link/93ac0c50dd620dc7b88e5fe05c70e15b">bar link text</a>;</li>
</ol>
<p>Paragraph text baz and biz text.</p><div class="aritcle_card flexRow">
<div class="artcardd flexRow">
<a class="aritcle_card_img" href="/ai/2400" title="星月写作"><img
src="https://img.php.cn/upload/ai_manual/001/246/273/176369517165546.png" alt="星月写作" onerror="this.onerror='';this.src='/static/lhimages/moren/morentu.png'" ></a>
<div class="aritcle_card_info flexColumn">
<a href="/ai/2400" title="星月写作">星月写作</a>
<p>专为网络小说、 剧本创作者打造的AI增效工具</p>
</div>
<a href="/ai/2400" title="星月写作" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span> </a>
</div>
</div><p><span>立即学习</span>“<a href="https://pan.quark.cn/s/cb6835dc7db1" style="text-decoration: underline !important; color: blue; font-weight: bolder;" rel="nofollow" target="_blank">前端免费学习笔记(深入)</a>”;</p>
<p>Paragraph text.</p>
`;
html = `<body>${html}</body>`;
const parsed = new DOMParser().parseFromString(html, 'text/html');
function flattenNonLiElements(node) {
for (const child of node.childNodes) {
// 先递归处理子节点(深度优先)
flattenNonLiElements(child);
// 仅对元素节点进行判断和替换
if (child.nodeType !== Node.ELEMENT_NODE) continue;
// 仅保留 <li>,其余元素全部展开为子节点(含文本、注释等)
if (child.nodeName.toLowerCase() !== 'li') {
// 注意:replaceWith(...) 会将 child 替换为其所有直接子节点
// 若 child 无子节点(如空 <p></p>),则被替换为空白(即消失)
child.replaceWith(...child.childNodes);
}
}
}
flattenNonLiElements(parsed.body);
console.log(parsed.body.innerHTML);
// 输出:
// <li>foo link text;</li>
// <li>bar link text;</li>
// Paragraph text baz and biz text.
// Paragraph text.✅ 关键要点总结:
- ❌ 避免在 DOM 修改过程中使用 forEach() 遍历 childNodes;
- ✅ 优先选用 for...of 或传统 for (let i = 0; i
- ✅ replaceWith(...childNodes) 是安全展开元素内容的标准方式,天然支持混合节点类型(文本、元素、注释);
- ⚠️ 注意:若目标元素(如
)内含嵌套 HTML(如
),它们也会被一并展开——这正是需求所要求的“只留文本”效果;如需保留特定内联标签,需额外白名单逻辑; - ? 建议在真实项目中添加边界检查(如 node && node.childNodes),增强鲁棒性。
该方案简洁、可预测,适用于任意层级 HTML 结构的语义化精简处理。









