在 Web 前端开发中,与“选区”和“光标”打交道是家常便饭。无论是实现文本高亮、自定义工具栏,还是精确控制光标位置,都离不开对它们底层原理的理解。选区通常指的是用户用鼠标拖拽选中的蓝色区域。而光标,不就是那个闪烁的竖线吗?
先说一个核心结论:光标其实是一种特殊的选区。
要搞懂这一点,必须请出两位关键角色:Selection 和 Range 对象。它们拥有丰富的属性和方法,这里我们先快速认识一下:
- Selection 对象代表了用户选择的文本范围或当前插入符号(光标)的位置。它可能横跨页面中的多个元素,通常由用户拖拽鼠标产生。
- Range 对象则表示一个包含节点与部分文本节点的文档片段。我们操作光标和选区,真正打交道更多的是通过
Selection 获取到的 Range 对象。
要获取当前的选区,可以使用全局的 window.getSelection() 方法。
const selection = window.getSelection();

通常我们不直接操作 selection 对象,而是操作它对应的 range。获取方式如下:
const range = selection.getRangeAt(0);

你可能会问,getRangeAt 为什么要传一个索引?难道选区还能有多个?是的,虽然目前只有 Firefox 浏览器通过按住 Cmd (Windows 上是 Ctrl) 键支持多选区,但 API 确实为此做了设计。

获取选中的文本内容很简单,直接调用 toString() 即可。
window.getSelection().toString()
// 或者
window.getSelection().getRangeAt(0).toString()

关键属性来了:range.collapsed。这个属性表示选区的起点和终点是否重合。当 collapsed 为 true 时,选区被压缩成一个点。在普通元素上,这个点不可见;但在可编辑元素上,这个点就显现为闪烁的光标。

所以,光标就是起始点相同的选区。
可编辑元素:操作的主战场
虽然选区本身与元素是否可编辑无关,但光标只在可编辑元素上可见。我们的大部分需求也集中在这里。
可编辑元素主要分两类:
- 默认的表单输入框:
<input type="text"> 和 <textarea>。
- 通过添加
contenteditable="true" 属性或 CSS 属性 -webkit-user-modify: read-write; 使普通元素(如 <div>)可编辑。
这两者的主要区别在于,表单输入框的 API 更直观、更容易控制。
轻松上手:Input 和 Textarea 的选区操作
对于 input 和 textarea,浏览器提供了更友好的专属 API,我们可以暂时忘记复杂的 Selection 和 Range。让我们通过例子来学习,假设我们有如下 HTML:
<textarea id="txt">阅文旗下囊括 QQ 阅读、起点中文网、新丽传媒等业界知名品牌,拥有 1450 万部作品储备,940 万名创作者,覆盖 200 多种内容品类,触达数亿用户,已成功输出包括《庆余年》《赘婿》《鬼吹灯》《琅琊榜》《全职高手》在内的动画、影视、游戏等领域的 IP 改编代表作。</textarea>
1. 主动选中某一区域
使用 setSelectionRange(start, end) 方法。
btn.onclick = () => {
txt.setSelectionRange(0,2); // 选中前两个字“阅文”
txt.focus();
}

如果想选中全部内容,可以直接调用 select() 方法。
2. 移动光标到特定位置
既然光标是 collapsed 的选区,那我们只需将起始点设为相同即可。
btn.onclick = () => {
txt.setSelectionRange(2,2); // 光标移动到“阅文”后面
txt.focus();
}

3. 还原之前的选区
需要先记录选区位置,使用 selectionStart 和 selectionEnd 属性。
const pos = {}
document.onmouseup = (ev) => {
pos.start = txt.selectionStart;
pos.end = txt.selectionEnd;
}
btn.onclick = () => {
txt.setSelectionRange(pos.start,pos.end)
txt.focus();
}

4. 在选区插入或替换内容
使用 setRangeText() 方法,功能强大。
btn.onclick = () => {
// 在当前选区插入爱心表情
txt.setRangeText('❤️❤️❤️')
txt.focus();
}

setRangeText 还可以指定替换的起始位置和完成后的光标行为(select, start, end, preserve)。
以上关于表单元素操作的完整示例,可以访问 setSelectionRange & setRangeText (CodePen)。
进阶挑战:普通(可编辑)元素的选区操作
普通元素(如 contenteditable 的 div)没有上述表单元素的便捷 API,我们必须回到 Selection 和 Range 的怀抱。这里的 API 更多,但理解了原理后便能融会贯通。
1. 主动选中纯文本区域的某一部分
假设我们有一个可编辑的 div,内容与上面的 textarea 相同。要选中前两个字“阅文”,步骤如下:
btn.onclick = () => {
const selection = document.getSelection();
const range = document.createRange();
range.setStart(txt.firstChild, 0); // 关键:起始节点是文本节点
range.setEnd(txt.firstChild, 2);
selection.removeAllRanges();
selection.addRange(range);
}

注意:setStart 和 setEnd 的第一个参数是 txt.firstChild(文本节点),而不是 txt(元素节点)。这是因为对于文本节点,偏移量指的是字符数;对于元素节点,偏移量指的是子节点索引。
2. 选中富文本(内嵌标签)中的区域
这才是普通元素选区复杂性的根源。考虑如下 HTML:
<div id="txt" contenteditable="true">yux<span>阅文</span>前端</div>
要选中“阅文”这个 span,可以使用 range.selectNode(node) 或 range.selectNodeContents(node)。两者区别在于前者选中节点自身(包括标签),后者仅选中节点内部内容。
btn.onclick = () => {
const selection = document.getSelection();
const range = document.createRange();
range.selectNode(txt.childNodes[1]) // 选中第二个子节点,即span元素
selection.removeAllRanges();
selection.addRange(range);
}
3. 实现“按字符偏移量选中”的通用方法
在实际开发中,我们更希望像操作 textarea 一样,直接指定相对于外层元素文本的起止位置(如 [2,5]),而不用关心内部复杂的 DOM 结构。这需要我们自己实现一个遍历算法。
核心思路是:递归提取所有 #text 文本节点,计算每个文本节点在整个文本中的字符区间,然后根据目标偏移量定位到具体的文本节点和节点内的偏移量。
function getNodeAndOffset(wrap_dom, start=0, end=0){
const txtList = [];
const map = function(children){
[...children].forEach(el => {
if (el.nodeName === '#text') {
txtList.push(el)
} else {
map(el.childNodes)
}
})
}
// 递归遍历,提取出所有 #text
map(wrap_dom.childNodes);
// 计算文本的位置区间 [0,3]、[3, 8]、[8,10]
const clips = txtList.reduce((arr,item,index)=>{
const end = item.textContent.length + (arr[index-1]?arr[index-1][2]:0)
arr.push([item, end - item.textContent.length, end])
return arr
},[])
// 查找满足条件的范围区间
const startNode = clips.find(el => start >= el[1] && start < el[2]);
const endNode = clips.find(el => end >= el[1] && end < el[2]);
return [startNode[0], start - startNode[1], endNode[0], end - endNode[1]]
}
有了这个工具函数,无论 div 内部的 HTML 结构多复杂,我们都可以轻松选中指定字符区间。

4. 其他常见操作
基于上述原理和工具函数,其他操作如移动光标、还原选区、插入内容、包裹标签等都可以实现。例如:
- 插入内容:使用
range.deleteContents() 删除原选区,再用 range.insertNode() 插入新节点(可以是 document.createTextNode 创建的文本节点,或 document.createElement 创建的元素节点)。
- 包裹标签:虽然
range.surroundContents() 是官方 API,但当选区不连续时会报错。更稳健的方法是使用 range.extractContents() 提取选区内容到一个 DocumentFragment,然后将其放入新创建的元素中,再用 range.insertNode() 插回。
掌握这些核心的 DOM 操作方法,你就能在前端开发中游刃有余地处理任何选区与光标相关的交互需求。虽然现代前端框架封装了许多便捷功能,但在实现富文本编辑器、自定义输入体验等复杂场景时,这些“原生力量”是不可或缺的。
如果你想在代码实践中深入探索,可以访问相关的 前端 & 移动 开发板块,那里有更多实战案例和社区讨论。
一图胜千言:核心 API 总结
最后,用两张图来梳理一下针对不同元素的核心操作 API,方便记忆和查阅:
针对 Input & Textarea:

针对普通可编辑元素:

本文覆盖了 Selection 和 Range API 在常见开发场景中的绝大部分应用。要了解更多细节和边缘情况,随时查阅 MDN 官方文档总是最好的选择。希望这篇长文能帮你彻底理清 Web 中的选区与光标,欢迎在 云栈社区 分享你的实践经验或提出问题。