如何用纯JavaScript高亮元素内匹配搜索词的文本且不影响元素属性
实现思路
要实现不修改元素属性的搜索高亮,核心原则是仅操作DOM树中的纯文本节点,完全避开标签、属性的内容处理,步骤如下:
- 每次触发搜索前先清理之前添加的
<mark>高亮标签,避免多次搜索出现标签嵌套 - 递归遍历目标元素的所有子节点:遇到元素节点就继续遍历其子节点,遇到纯文本节点再做内容匹配
- 对匹配到的搜索词,通过DOM节点拆分的方式插入
<mark>标签,而非直接修改innerHTML
完整可运行代码
<input type='text' id='search' onkeyup="highlight(this.value)"> <p id='WE1'> <b>WE</b>wE & they <a href="url_With_WE_we_wE_WeOnly">them and We</a> <span id="we2">we only.</span> </p> <script> function highlight(searchedWords) { const targetEl = document.getElementById("WE1"); // 先清空之前的高亮:释放mark标签内的内容,删除mark标签 clearHighlight(targetEl); const words = searchedWords.trim().split(/\s+/).filter(w => w.length > 0); if (words.length === 0) return; // 生成全局匹配正则,大小写不敏感,多个搜索词用|分隔 const regex = new RegExp(words.map(w => escapeRegExp(w)).join('|'), 'gi'); // 递归遍历所有文本节点做替换 traverseTextNodes(targetEl, (textNode) => { const text = textNode.nodeValue; const matches = text.match(regex); if (!matches) return; // 拆分文本节点,插入mark标签 let lastIndex = 0; const fragment = document.createDocumentFragment(); text.replace(regex, (match, index) => { // 插入匹配位置之前的文本 if (index > lastIndex) { fragment.appendChild(document.createTextNode(text.slice(lastIndex, index))); } // 插入带高亮的匹配内容 const mark = document.createElement('mark'); mark.textContent = match; fragment.appendChild(mark); lastIndex = index + match.length; }); // 插入最后剩余的文本 if (lastIndex < text.length) { fragment.appendChild(document.createTextNode(text.slice(lastIndex))); } // 替换原来的文本节点 textNode.parentNode.replaceChild(fragment, textNode); }); } // 工具函数:转义正则特殊字符,避免搜索词包含./*等字符时报错 function escapeRegExp(str) { return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } // 工具函数:清理之前的高亮 function clearHighlight(el) { const marks = el.querySelectorAll('mark'); marks.forEach(mark => { const parent = mark.parentNode; parent.replaceChild(document.createTextNode(mark.textContent), mark); // 合并相邻文本节点,避免节点碎片化 parent.normalize(); }); } // 工具函数:遍历所有纯文本节点,跳过不需要处理的元素 function traverseTextNodes(el, callback) { const childNodes = el.childNodes; for (let i = 0; i < childNodes.length; i++) { const node = childNodes[i]; // 跳过mark、script、style等无需处理的元素 if (node.nodeType === Node.ELEMENT_NODE) { if (['MARK', 'SCRIPT', 'STYLE', 'IFRAME'].includes(node.tagName)) continue; traverseTextNodes(node, callback); } else if (node.nodeType === Node.TEXT_NODE && node.nodeValue.trim().length > 0) { callback(node); } } } </script>
方案优势
- 完全不会修改元素属性:全程仅操作纯文本节点,不会触及标签、href、id等任何属性内容,a标签等元素完全不受影响
- 大小写不敏感:正则使用
gi标识,匹配时忽略大小写,同时保留原始文本的大小写展示 - 兼容性好:所有API都是DOM基础API,兼容所有主流浏览器,不存在
replaceAll的兼容性问题 - 支持特殊字符搜索:增加了正则转义逻辑,搜索词包含
.、*、+等正则特殊字符也不会报错 - 多次搜索不会错乱:每次搜索前先清空之前的高亮,不会出现标签嵌套问题
内容的提问来源于stack exchange,提问作者Dante Valenzuela
相关产品推荐
相关产品推荐

