
本文深入解析《Eloquent JavaScript》第14章中 talksAbout 递归函数的工作原理,揭示其在无目标文本时仍返回 true 的根本原因——即脚本节点自身被意外纳入遍历范围,帮助开发者避开 DOM 递归遍历中易被忽视的执行上下文陷阱。
本文深入解析《eloquent javascript》第14章中 `talksabout` 递归函数的工作原理,揭示其在无目标文本时仍返回 `true` 的根本原因——即脚本节点自身被意外纳入遍历范围,帮助开发者避开 dom 递归遍历中易被忽视的执行上下文陷阱。
talksAbout(node, string) 是一个典型的 DOM 树深度优先递归搜索函数:它遍历 HTML 节点树,对每个元素节点(Node.ELEMENT_NODE)递归检查其子节点;对文本节点(Node.TEXT_NODE)则直接用 indexOf() 判断是否包含目标字符串。表面上逻辑清晰,但实际运行中却出现“删除 'book' 后仍返回 true”的反直觉现象——问题根源在于调用位置。
关键在于:你将 document.body 作为起始节点传入,但 <script> 标签本身是 <body> 的最后一个子节点(即使写在 HTML 底部,浏览器解析时也会将其插入 body 内)。当 talksAbout 遍历 body.childNodes 时,最终会抵达该 <script> 元素;而 <script> 元素的子节点包含一段文本节点——即脚本内部的源码字符串:
console.log(talksAbout(document.body, "book"));
这段代码本身含有子字符串 "book"!因此,当递归进入 <script> 节点后,其 textContent(或 nodeValue)匹配成功,函数立即返回 true。
✅ 正确做法:排除 <script> 节点或限定搜索范围
最稳妥的方式是避免将 <script> 纳入搜索起点。例如,只搜索 <body> 的非脚本子元素:
// ✅ 推荐:过滤掉 script 和 style 节点
function talksAbout(node, string) {
if (node.nodeType === Node.ELEMENT_NODE) {
// 跳过 script/style 标签,防止匹配自身代码
if (node.tagName === 'SCRIPT' || node.tagName === 'STYLE') {
return false;
}
for (let child of node.childNodes) {
if (talksAbout(child, string)) return true;
}
return false;
} else if (node.nodeType === Node.TEXT_NODE) {
return node.nodeValue.includes(string); // 使用 includes 更语义化
}
return false;
}⚠️ 注意事项:
- node.childNodes 包含所有子节点(包括文本、注释、元素),而 node.children 仅返回元素节点——若需简化逻辑,可优先使用 children 迭代,再单独处理文本内容;
- 浏览器自动将 <script> 插入 DOM 树中,这是标准行为,而非 bug;
- 调试时善用 console.log(node.nodeName, node.nodeType, node.nodeValue?.slice(0,20)) 可快速定位误匹配节点;
- 生产环境建议结合 textContent(忽略标签)与正则模糊匹配,提升鲁棒性。
总结:递归 DOM 搜索必须明确作用域边界。talksAbout 的设计初衷是查找用户可见文本,而非代码自身——意识到脚本节点的存在,并主动排除,是编写健壮前端递归工具的第一步。

















