初识 Range 对象
<style>.artBox { margin: 20px auto; font: normal 18px/28px sans-serif; max-width: 1200px; }
.artBox mark { padding: 2px 6px; background: lightblue; }
.artBox blockquote { padding: 14px; border: solid lightblue; border-width: 0 4px; border-radius: 10px; background: beige; }
.artBox p { margin: 10px 0; }
.mama { margin: 30px; width: 600px; border: 1px solid gray; border-radius: 10px; padding: 10px; }
</style>
<div class="artBox">
<blockquote>Range 接口表示一个包含节点与文本节点的一部分的文档片段。(——MDN官网的定义)<br><br>Selection 和 Range 都是现代浏览器原生提供的对象和 API,一般通称为 Selection 和 Range API(接口)。Selection(选区)是“视图状态”(用户在屏幕上高亮了什么),Range(范围)是“数据模型”(描述选了什么)。换言之,Range 是 Selection 的基本概念,Range 的本质是对一对“边界点”的描述——范围的起点和范围的终点。通俗说来,Selection 是用户“拖蓝”的视觉效果,Range 则是“拖蓝”背后的数据描述;另外,很重要的,Selection 和 Range 的关系是包含关系,具体而言,Selection 是 Range 的容器,尽管有时候 Range 可以不依托显性提供的 Selection 而独立存在。</blockquote>
<p>通常,Selection 和 Range 总是一同出现的:先获取 Selection 对象,然后通过 Range 来做什么。其一,使用 Range 读取基于 Selection 的相关数据(例如锚点和锚点偏移量、焦点和焦点偏移量等),其二,创建新的范围并添加到 Selection 对象中。不过,Range 在特定场合可以不依赖 Selection,可以独立使用。学习 Range 需要了解其构造、属性和方法。</p>
<h3>一、Range的获取和构造</h3>
<div class="hEdiv"><pre class="hEpre">
// 获取选区 Range(参数为选区序号)
Selection.getRangeAt(index)
// 创建 Range 方法一
document.createRange()
// 创建 Range 方法二
new Range()
</pre></div>
<h3>二、Range的常用属性</h3>
<ol>
<li><b>collapsed</b> : 只读,返回 Range 的起始位置和终止位置是否相同(true/false)</li>
<li><b>commonAncestorContainer</b> :只读,返回完整包含 startContainer 和 endContainer 的最深一级的节点</li>
<li><b>endContainer</b> :只读,返回包含 Range 终点的节点</li>
<li><b>endOffset</b> :只读,返回 Range 终点在 endContainer 中的位置</li>
<li><b>startContainer</b> :只读,返回包含 Range 开始的节点</li>
<li><b>startOffset</b> :只读,返回 Range 在 startContainer 中的起始位置</li>
</ol>
<h3>三、Range的常用方法</h3>
<ol>
<li><b>collapse()</b> :将 Range 折叠到其边界的端点</li>
<li><b>compareBoundaryPoints()</b> :将 Range 的边界与另一个 Range 的边界进行比较</li>
<li><b>compareNode() </b> :返回一个常量,表示节点是否在 Range 的前、后、中、外</li>
<li><b>comparePoint() </b> :返回 -1、0 或 1,分别表示端点在 Range 之前、内部还是之后</li>
<li><b>cloneContents() </b> :返回一个复制 Range 中所有节点的文档片段</li>
<li><b>cloneRange() </b> :返回一个拥有和原 Range 对象相同端点的克隆的 Range 对象</li>
<li><b>createContextualFragment()</b> :返回从给定的代码字符串创建的文档片段</li>
<li><b>deleteContents()</b> :从 Document 中移除 Range 内容</li>
<li><b>detach()</b> :将 Range 从使用状态释放,提高性能</li>
<li><b>extractContents()</b> :将 Range 的内容从文档树移动到一个文档片段</li>
<li><b>getBoundingClientRect()</b> :返回一个 DOMRect 对象,其绑定了 Range 的整个内容;这将是通过 range.getClientRects() 返回所有边界矩形集合的其中之一</li>
<li><b>getClientRects()</b> :返回一个 DOMRect 列表对象,该列表汇总了 Range 中所有元素的 Element.getClientRects() 返回结果</li>
<li><b>isPointInRange()</b> :返回一个 boolean,表示点端点是否在 Range 中</li>
<li><b>insertNode()</b> :在 Range 开头插入一个节点</li>
<li><b>intersectsNode()</b> :返回一个 boolean,表示给定的节点是否与 Range 相交</li>
<li><b>selectNode()</b> :设置 Range 包含某个节点及其他的内容</li>
<li><b>selectNodeContents() </b> :设置 Range 包含某个节点的内容</li>
<li><b>setEnd()</b> :设置 Range 的终点</li>
<li><b>setStart()</b> :设置 Range 的起点</li>
<li><b>setEndAfter()</b> :以另一个节点为基准,设置 Range 的终点位置</li>
<li><b>setEndBefore()</b> :以另一个节点为基准,设置 Range 的终点位置</li>
<li><b>setStartAfter()</b> :以另一个节点为基准,设置 Range 的起点位置</li>
<li><b>setStartBefore()</b> :以另一个节点为基准,设置 Range 的起点位置</li>
<li><b>surroundContents()</b> :将 Range 中的内容移动到一个新的节点</li>
<li><b>toString()</b> :返回 Range 中的文本</li>
</ol>
<h3>四、应用实例</h3>
<p><b>【例一】</b>在简单文本节点中创建选区范围并“拖蓝”</p>
<div class="hEdiv" data-prev="1"><pre class="hEpre">
<!-- 下面的 p 标签仅包含一个 #Text 节点,本例目标是将 “花朝论坛” 拖蓝 -->
<p id="p1">花朝论创建于哪一年?</p>
<p><button onclick="selectText(p1,0,4)">创建选区(0~4)</button></p>
<script>
// 创建选区函数(仅适用于理想型文本节点 —— #Text节点)
// 参数 start :选区范围起点位置;end :选区范围结束位置
// 例如,0~4表示选取范围从文本节点第一个字符的左边起算,到第4个字符的右边结束
function selectText(elm, start, end) {
const range = new Range(); // 创建范围
const node = p1.firstChild; // 指定节点(p1的第一个文本节点)
range.setStart(node, start); // 范围从 node 节点的第 start 位置开始
range.setEnd(node, end); // 范围从 node 节点的第 end 偏移量结束
const sel = window.getSelection(); // 获取选区对象
sel.removeAllRanges(); // 清除选区之前已有全部 Range 范围
sel.addRange(range); // 在选区中添加新创建的 Range
}
</script>
</pre></div>
<blockquote>当确定待处理的节点是文本节点(#text),Range 的 <mark>setStart(节点, 位置)</mark> 和 <mark>setEnd(节点, 位置)</mark> 方法中的<b>位置</b>参数以<b>字符索引</b>做参照,0 表示节点内第一个字符的前面的位置(就像在新建一个Word文档时输入光标的位置),1 表示节点内第一个字符和第二个字符之间的位置,2 表示第二个字符和第三个字符之间的位置,其它以此类推。</blockquote>
<p><b>【例二】</b>在复杂的混合节点环境中创建选区</p>
<div class="hEdiv" data-prev="1"><pre class="hEpre">
<!-- 下面的 p 标签有文本节点和元素节点,环境复杂,共 5 个子节点 -->
<!-- 节点索引0. #text : 才思敏捷、多才多艺的 -->
<!-- 节点索引1. B :<b>樵歌</b> -->
<!-- 节点索引2. #text :是 -->
<!-- 节点索引3. MARK :<mark>花潮论坛</mark> -->
<!-- 节点索引4. #text :的联合创建者之一 -->
<p id="p2">才思敏捷、多才多艺的<b>樵歌</b>是<mark>花潮论坛</mark>的联合创建者之一</p>
<p><button onclick="selectText(p2,0,4)">创建选区(0~4)</button></p>
<script>
// 创建选区函数(适用于混合节点环境,若子节点是#text会全选)
// 参数 start :选区范围起点节点;end :选区范围结束节点(数孩子节点索引而不是字符索引)
// 例如,0~4表示选取范围从0号索引节点(对应第一个孩子节点)前起算,
// 到第4个孩子节点(对应第五个孩子节点)之前结束
function selectText(elm, start, end) {
const sel = window.getSelection(); // 获取选区对象
const range = new Range(); // 创建范围
const total = p2.childNodes.length; // 获取子节点总数
//console.log(total); // -> 5
if (end > total) end = total; // 结束节点索引号不能超出总子节点数量
range.setStart(p2, start); // 范围从 p2 中索引号号是第 start 孩子节点前开始
range.setEnd(p2, end); // 范围从 p2 中索引号是第 end 孩子节点前结束
sel.removeAllRanges(); // 清除选区已有全部 Range 范围
sel.addRange(range); // 添加新创建的 Range
}
</script>
</pre></div>
<blockquote>如果确定容器内的子节点为混合节点,对子节点的处理变为复杂,Range 的 setStart 和 setEnd 方法的<b>位置</b>参数表示的是子节点在容器中的索引序号,序号从 0 开始,换言之,位置参数不是指代字符索引,而是指向子节点序号。这是 Selection + Range API的浏览器底层设计。<br>如果需要精准按字符索引实现拖蓝或读取拖蓝数据,需要遍历所有子节点、找出最底层的 #text 并通过相应算法加以实现。</blockquote>
<p><b>【例三】</b>利用 Range 在指定节点前添加新的文本节点</p>
<div class="hEdiv" data-prev="1"><pre class="hEpre">
<style>
#mdiv { margin: 20px auto 0; width: 600px; font-size: 30px; border: 1px solid gray; padding: 12px; }
</style>
<div id="mdiv" title="点击补全句子">只怕有心人</div>
<script>
const mdiv = document.getElementById('mdiv');
mdiv.onclick = () => {
//console.log(mdiv.childNodes.length); // -> 1
const txt = '世上无难事 '; // 待创建文本节点内容
if (mdiv.innerText.includes(txt)) return; // 避免重复补全
const txtNode = document.createTextNode(txt); // 创建文本节点
const range = new Range(); // 创建新 Range 对象
range.selectNode(mdiv.firstChild); // 选择 mdiv 容器第一个子节点为 range
range.insertNode(txtNode); // range 前插入新创建的文本节点
//console.log(mdiv.childNodes.length); // -> 2
};
</script>
</pre></div>
<blockquote>Range 的 <mark>selectNode()</mark> 方法将 Range 设置为包含整个 Node 及其内容。<mark>insertNode()</mark> 方法在 Range 的起始位置插入新的节点。</blockquote>
<p><b>【例四】</b>借助 Range 创建范围删除表格指定行</p>
<div class="hEdiv" data-prev="1"><pre class="hEpre">
<style>
#tab { width: 600px; border-collapse: collapse; }
#tab th, #tab td { border: 1px solid #ccc; padding: 8px; }
#tab th { background: #ddd; }
</style>
<p><button onclick="delTabRow();">删除表格的最后一行</button></p>
<table id="tab">
<tr>
<th>表头单元格一</th>
<th>表头单元格二</th>
</tr>
<tr>
<td>内容1</td>
<td>内容2</td>
</tr>
<tr>
<td>内容3</td>
<td>内容4</td>
</tr>
<tr>
<td>内容5</td>
<td>内容6</td>
</tr>
<tr>
<td>内容7</td>
<td>内容8</td>
</tr>
</table>
<script>
// 删除表格最后一行
function delTabRow() {
const tab = document.getElementById('tab'); // 获得表格dom实体
const rest = tab.rows.length - 1; // 最后一行
if (rest <= 0) return; // 如果表格仅剩一行放过(不执行下面的代码)
const row = tab.rows; // 获取表格的第后一行
const range = document.createRange(); // 创建 Range 范围
range.setStartBefore(row); // Range 起点设在表格最后一行的前边
range.setEndAfter(row); // Range 终点设在表格最后一行的后边
range.deleteContents(); // 删除 Range 范围包裹的内容(即 <tr>...</tr>
}
</script>
</pre></div>
<blockquote>Range 对象的 <mark>setStartBefore(节点)</mark> 和 <mark>setStartAfter(节点)</mark> 方法在指定节点前、后创建范围,<mark>deleteContents()</mark> 方法则删除范围内的内容。</blockquote>
<p><b>【例五】</b>搬运元素节点</p>
<div class="hEdiv" data-prev="1"><pre class="hEpre">
<style>
.mydiv { margin: 20px; padding: 12px; border: 1px solid gray; width: 600px; min-height: 100px; }
.mydiv > p { margin: 0; padding: 0; }
</style>
<h2>单击第一个方框</h2>
<div id="d1" class="mydiv" onclick="moveline();">
<p>红豆生南国,</p>
<p>春来发几枝。</p>
<p>愿君多采撷,</p>
<p>此物最相思。</p>
</div>
<div id="d2" class="mydiv"></div>
<script>
const rawHTML = d1.innerHTML; // d1 原始代码
// 逐行移动p标签到另一个方框
function moveline() {
let lock = false; // 搬运锁
// 如果搬完了
if (d1.innerHTML.trim() === '') {
d1.innerHTML = rawHTML; // 重置 d1 内容
d2.innerHTML = ''; // d2 清空
lock = true; // 上锁
}
if (lock) return; // 上锁状态下不执行下面的任务
// 核心 :将 d1 的第一个 p 标签移动到 d2
const ps = d1.querySelectorAll('p'); // 获得所有 p 标签节点
if (ps.length < 1) return; // p 标签没了就不要继续干活(会犯错误)
const range = document.createRange(); // 创建 Range 范围
range.selectNode(ps); // Range 选择第一个 p 节点
const frg = range.extractContents(); // 提取 p 节点内容(会从原始宿主中删除节点)加入文档碎片
d2.appendChild(frg); // 文档碎片追加给 d2
}
</script>
</pre></div>
<blockquote>Range 的 <mark>extractContents()</mark> 方法提取 Range 范围内的节点(可以是文本或元素,例如上例的第一个 p 标签),并<b>删除原始范围</b>,返回的是文档碎片(相当于 DocumentFragment API 生成的碎片),该碎片可以追加给另外的目标元素。</blockquote>
<p>Range 方法较多并且基本都很抽象、复杂,不过学习难度不在其所提供的方法自身,这些方法通过潜心学习并不断实践总能融会贯通,难的是编程方法——比如如何将相关知识有机结合起来去实现预定目标。</p
<p>本文提供的几个实例不可能覆盖所有的 Range 知识,若能从中获得一些启发和灵感便达到本文的目的。</p>
</div>
<script id="js_1">
</script>
<script type="module">
import hl from 'https://638183.freep.cn/638183/web/js/hl.js';
const divs = document.querySelectorAll('.hEdiv');
const pres = document.querySelectorAll('.hEpre');
divs.forEach((div,idx) => hl.hl(div, pres));
</script> 本帖尚未仔细校对,可能会对存在的问题随时做出修正 谢谢黑黑老师辛苦,学习了。{:4_187:} 好复杂,精彩的分享{:4_187:} Range的常用方法还挺多的呢,案例里看到的实现效果也很奇特,可以实现很多预定目标呢。
黑黑的讲解辛苦了,学习了{:4_187:} 红影 发表于 2026-7-4 16:20
Range的常用方法还挺多的呢,案例里看到的实现效果也很奇特,可以实现很多预定目标呢。
黑黑的讲解辛苦了 ...
Range 确实很麻烦。好在做个帖子用不到它,了解一下就好 玫瑰 发表于 2026-7-4 13:38
好复杂,精彩的分享
是不简单 梦江南 发表于 2026-7-4 11:47
谢谢黑黑老师辛苦,学习了。
{:4_190:}
页:
[1]