必须先创建Range实例并用setStart()/setEnd()精确定位字符级范围;直接selectNode()无法精确定位,surroundContents()限制严格需手动拆解操作。

如何用 document.createRange() 创建可操作的文本 Range
必须先创建 Range 实例,再设置起点和终点;直接调用 range.selectNode() 或 range.selectNodeContents() 无法精确定位到字符级,只适合整节点操作。
常见错误是创建后没调用 setStart() 和 setEnd() 就尝试 surroundContents(),结果报错 InvalidStateError: The range is not collapsed and does not select a single node。
- 起点和终点必须落在文本节点(
Node.TEXT_NODE)或元素节点上,但跨节点时需确保它们有共同祖先 - 偏移量是相对于节点内容的字符/子节点索引:对文本节点是 Unicode 字符位置,对元素节点是子节点序号
- 若目标文本被包裹在多个嵌套标签里(如
<span><em>text</em></span>),需先遍历找到最内层的文本节点,再计算其内部偏移
如何定位并设置文本节点内的精确字符范围
不能靠 innerText 或 textContent 的字符串索引直接映射——DOM 树结构会导致字符偏移与线性字符串不一致。正确做法是递归遍历文本节点,累加长度直到覆盖目标起止位置。
例如想高亮 “hello world” 中的 “world”,不能写 range.setStart(textNode, 6) 就完事;如果前面有 <strong>hello</strong> world,那 “world” 实际在第二个文本节点里,偏移是 0,不是 6。
立即学习“前端免费学习笔记(深入)”;
- 用
NodeIterator或深度优先遍历获取所有文本节点,并记录累计字符长度 - 用
textNode.textContent.length算单个节点长度,注意\r\n算两个字符,Unicode emoji(如 ??)可能占多个码点,但.length返回的是 UTF-16 码元数,通常够用 - 一旦定位到目标文本节点和偏移,立刻用
range.setStart(textNode, startOffset)和range.setEnd(textNode, endOffset)
为什么 range.surroundContents() 经常失败
这个方法表面方便,实则限制极严:要求 Range 必须完全位于**单个元素节点内**,且不能跨文本节点、不能包含元素边界、不能起点终点在不同父节点下。哪怕多一个空格文本节点,就会抛 InvalidStateError。
更稳妥的替代方案是手动拆解:range.extractContents() 拿出片段 → 创建 wrapper 元素(如 document.createElement('mark'))→ wrapper.appendChild(extracted) → range.insertNode(wrapper)。
-
extractContents()会从 DOM 移除原内容,返回 DocumentFragment,可安全插入新容器 - 若需保留原始结构(比如高亮时不破坏 inline 样式),建议用
cloneContents()+deleteContents()+insertNode()组合 - 注意:操作后原 Range 的
startContainer和endContainer可能已不存在,后续不能再复用该 Range 实例
修改 DOM 后 Range 失效的典型场景
Range 不是活引用,它绑定的是创建时刻的节点快照。一旦父元素被 innerHTML = ...、removeChild() 或框架重渲染(如 React 更新)修改,Range 的 startContainer 可能变成 null 或指向已移除节点,调用 getBoundingClientRect() 会返回全零,collapsed 可能误判为 true。
- 不要缓存 Range 实例超过一次用户交互周期;高频操作(如输入时实时高亮)应在每次触发时重新创建
- 若必须复用,操作前检查
range.startContainer.parentNode是否仍存在,或用range.commonAncestorContainer.contains(targetNode)做有效性预判 - 避免在
input事件中直接基于旧 Range 执行surroundContents()—— 输入可能已导致节点分裂(如光标处插入换行),原 Range 范围早已错位
white-space 设置都可能让显示效果和 Range 计算结果不一致,调试时务必用 console.log(range.toString()) 和 range.getBoundingClientRect() 对照验证。



















