马黑黑 发表于 2026-7-4 09:40

初识 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">
&lt;!-- 下面的 p 标签仅包含一个 #Text 节点,本例目标是将 “花朝论坛” 拖蓝 --&gt;
&lt;p id="p1"&gt;花朝论创建于哪一年?&lt;/p&gt;
&lt;p&gt;&lt;button onclick="selectText(p1,0,4)"&gt;创建选区(0~4)&lt;/button&gt;&lt;/p&gt;

&lt;script&gt;
    // 创建选区函数(仅适用于理想型文本节点 —— #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
    }
&lt;/script&gt;
    </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">
&lt;!-- 下面的 p 标签有文本节点和元素节点,环境复杂,共 5 个子节点 --&gt;
&lt;!-- 节点索引0. #text : 才思敏捷、多才多艺的 --&gt;
&lt;!-- 节点索引1. B :&lt;b&gt;樵歌&lt;/b&gt; --&gt;
&lt;!-- 节点索引2. #text :是 --&gt;
&lt;!-- 节点索引3. MARK :&lt;mark&gt;花潮论坛&lt;/mark&gt; --&gt;
&lt;!-- 节点索引4. #text :的联合创建者之一 --&gt;
&lt;p id="p2"&gt;才思敏捷、多才多艺的&lt;b&gt;樵歌&lt;/b&gt;是&lt;mark&gt;花潮论坛&lt;/mark&gt;的联合创建者之一&lt;/p&gt;
&lt;p&gt;&lt;button onclick="selectText(p2,0,4)"&gt;创建选区(0~4)&lt;/button&gt;&lt;/p&gt;

&lt;script&gt;
    // 创建选区函数(适用于混合节点环境,若子节点是#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); // -&gt; 5
      if (end &gt; total) end = total; // 结束节点索引号不能超出总子节点数量
      range.setStart(p2, start); // 范围从 p2 中索引号号是第 start 孩子节点前开始
      range.setEnd(p2, end); // 范围从 p2 中索引号是第 end 孩子节点前结束
      sel.removeAllRanges(); // 清除选区已有全部 Range 范围
      sel.addRange(range); // 添加新创建的 Range
    }
&lt;/script&gt;
    </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">
&lt;style&gt;
    #mdiv { margin: 20px auto 0; width: 600px; font-size: 30px; border: 1px solid gray; padding: 12px; }
&lt;/style&gt;

&lt;div id="mdiv" title="点击补全句子"&gt;只怕有心人&lt;/div&gt;

&lt;script&gt;
    const mdiv = document.getElementById('mdiv');
   
    mdiv.onclick = () =&gt; {
      //console.log(mdiv.childNodes.length); // -&gt; 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); // -&gt; 2
    };
&lt;/script&gt;
    </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">
&lt;style&gt;
    #tab { width: 600px; border-collapse: collapse; }
    #tab th, #tab td { border: 1px solid #ccc; padding: 8px; }
    #tab th { background: #ddd; }
&lt;/style&gt;

&lt;p&gt;&lt;button onclick="delTabRow();"&gt;删除表格的最后一行&lt;/button&gt;&lt;/p&gt;
&lt;table id="tab"&gt;
    &lt;tr&gt;
      &lt;th&gt;表头单元格一&lt;/th&gt;
      &lt;th&gt;表头单元格二&lt;/th&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;内容1&lt;/td&gt;
      &lt;td&gt;内容2&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
    &lt;td&gt;内容3&lt;/td&gt;
      &lt;td&gt;内容4&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;内容5&lt;/td&gt;
      &lt;td&gt;内容6&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
    &lt;td&gt;内容7&lt;/td&gt;
      &lt;td&gt;内容8&lt;/td&gt;
    &lt;/tr&gt;
&lt;/table&gt;

&lt;script&gt;
    // 删除表格最后一行
    function delTabRow() {
      const tab = document.getElementById('tab'); // 获得表格dom实体
      const rest = tab.rows.length - 1; // 最后一行
      if (rest &lt;= 0) return; // 如果表格仅剩一行放过(不执行下面的代码)
      const row = tab.rows; // 获取表格的第后一行
      const range = document.createRange(); // 创建 Range 范围
      range.setStartBefore(row); // Range 起点设在表格最后一行的前边
      range.setEndAfter(row); // Range 终点设在表格最后一行的后边
      range.deleteContents(); // 删除 Range 范围包裹的内容(即 &lt;tr&gt;...&lt;/tr&gt;
    }
&lt;/script&gt;
    </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">
&lt;style&gt;
    .mydiv { margin: 20px; padding: 12px; border: 1px solid gray; width: 600px; min-height: 100px; }
    .mydiv &gt; p { margin: 0; padding: 0; }
&lt;/style&gt;

&lt;h2&gt;单击第一个方框&lt;/h2&gt;
&lt;div id="d1" class="mydiv" onclick="moveline();"&gt;
    &lt;p&gt;红豆生南国,&lt;/p&gt;
    &lt;p&gt;春来发几枝。&lt;/p&gt;
    &lt;p&gt;愿君多采撷,&lt;/p&gt;
    &lt;p&gt;此物最相思。&lt;/p&gt;
&lt;/div&gt;
&lt;div id="d2" class="mydiv"&gt;&lt;/div&gt;

&lt;script&gt;
    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 &lt; 1) return; // p 标签没了就不要继续干活(会犯错误)
      const range = document.createRange(); // 创建 Range 范围
      range.selectNode(ps); // Range 选择第一个 p 节点
      const frg = range.extractContents(); // 提取 p 节点内容(会从原始宿主中删除节点)加入文档碎片
      d2.appendChild(frg); // 文档碎片追加给 d2
    }
&lt;/script&gt;
    </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>

马黑黑 发表于 2026-7-4 09:41

本帖尚未仔细校对,可能会对存在的问题随时做出修正

梦江南 发表于 2026-7-4 11:47

谢谢黑黑老师辛苦,学习了。{:4_187:}

玫瑰 发表于 2026-7-4 13:38

好复杂,精彩的分享{:4_187:}

红影 发表于 2026-7-4 16:20

Range的常用方法还挺多的呢,案例里看到的实现效果也很奇特,可以实现很多预定目标呢。
黑黑的讲解辛苦了,学习了{:4_187:}

马黑黑 发表于 2026-7-4 17:20

红影 发表于 2026-7-4 16:20
Range的常用方法还挺多的呢,案例里看到的实现效果也很奇特,可以实现很多预定目标呢。
黑黑的讲解辛苦了 ...

Range 确实很麻烦。好在做个帖子用不到它,了解一下就好

马黑黑 发表于 2026-7-4 17:21

玫瑰 发表于 2026-7-4 13:38
好复杂,精彩的分享

是不简单

马黑黑 发表于 2026-7-4 17:21

梦江南 发表于 2026-7-4 11:47
谢谢黑黑老师辛苦,学习了。

{:4_190:}
页: [1]
查看完整版本: 初识 Range 对象