深入对比 JavaScript 中 slice、substring 和 substr 三种截取字符串方法的区别。全面剖析参数定义、区间规则、负数索引机制、参数顺序倒置处理及 substr 的废弃现状,并提供现代 JavaScript 规范下的迁移与选型最佳实践。
JavaScript slice、substring和substr截取字符串的区别
时间:2026-11-02
浏览:417
文章目录
slice、substring 和 substr 都可以截取字符串片段,但核心机制有本质区别:第二个参数的含义不同(slice 与 substring 均表示结束字符的索引下标,而 substr 表示截取字符的长度);负数参数的处理逻辑不同(slice 将负数视为倒数索引,substring 直接将负数强制归零,substr 允许起始位置倒数但截取长度若为负直接返回空串);以及参数倒置时的表现不同(substring 会在起始索引大于结束索引时自动对调两数,而 slice 则严格返回空字符串)。值得特别注意的是,substr 属于 ECMAScript 附录 B(Annex B)中的已弃用特性,在现代前端与服务端开发中,应统一且优先使用 slice。
一、核心参数定义与方法速览
为了准确区分这三个方法,首先需要从它们的官方函数签名入手。很多初学者在调用方法时出现 Bug,最常见的原因就是记混了“第二个参数到底是结束下标还是提取长度”。
| 方法签名 | 第一参数含义 | 第二参数含义 | 截取区间性质 | 规范现状 |
|---|---|---|---|---|
str.slice(start, end) |
起始索引(含) | 结束索引(不含) | 左闭右开 [start, end) |
推荐标准 |
str.substring(start, end) |
起始索引(含) | 结束索引(不含) | 左闭右开 [min, max) |
标准保留 |
str.substr(start, length) |
起始索引(含) | 截取长度(字符数) | 连续抽取指定数量 | 已废弃 (Legacy) |
从签名即可看出:slice 与 substring 的语义是“切出两个下标之间的片段”,因此截取结果的长度取决于两参数之差;而 substr 的语义是“从指定位置向后拿几个字”,其第二参数直接控制了最终字符串的长度。
二、参数语义差异:截取区间 vs 指定长度
当传入正整数且起始位置小于结束位置时,slice 和 substring 的行为表现完全一致,但 substr 却大不相同。我们通过一个标准范例来观察两者的差异:
const str = "JavaScript";
// 1. slice: 截取下标 2 到 5 之间的字符 [2, 5),不包括下标 5
console.log(str.slice(2, 5)); // 输出: "vaS" (长度为 3)
// 2. substring: 同样截取下标 2 到 5 之间的字符 [2, 5)
console.log(str.substring(2, 5)); // 输出: "vaS" (长度为 3)
// 3. substr: 从下标 2 开始,向后连续提取 5 个字符
console.log(str.substr(2, 5)); // 输出: "vaScr" (长度为 5)
如图 1 所示,执行 str.slice(2, 5) 与 str.substring(2, 5) 时,提取的元素是下标为 2, 3, 4 的三个字符 "vaS",下标为 5 的字符 'c' 处于区间之外,不会被包含。而 str.substr(2, 5) 中的 5 代表的是数量,因此它从字符 'v' 开始连续向后读取 5 个字符,一直包含到下标 6 的 'r',得到 "vaScr"。
三、边界行为差异:负数索引与参数倒置机制
在处理动态计算的位置参数时,代码中经常会出现负数(如倒数读取字符)或大小顺序颠倒的情况。三种方法在面对这些极端与边界条件时有着截然不同的处理逻辑。
1. 负数索引的处理逻辑
-
slice(start, end)采用倒数索引机制:
如果参数为负数,slice会自动将其与字符串长度相加,即start = str.length + start。例如在长度为 10 的字符串上执行str.slice(-4),等同于str.slice(6),会截取末尾的 4 个字符"ript"。 -
substring(start, end)强制归零:
substring将任何小于 0 的数字或者NaN直接转为0。因此,str.substring(-4)会直接变成str.substring(0),返回整个字符串,这在很多场景下会违背开发者的初衷。 -
substr(start, length)起始位置支持负数,长度负数归零:
起始位置start为负数时同样从字符串末尾向前倒数;但第二个参数length表示的是长度,长度不能为负,任何<= 0的长度都会直接返回空字符串""。
const str = "JavaScript"; // length: 10
// 负数截取对比
console.log(str.slice(-4)); // 输出: "ript" (从倒数第4位一直到末尾)
console.log(str.substring(-4)); // 输出: "JavaScript" (负数被直接视作 0,即从 0 到末尾)
console.log(str.substr(-4)); // 输出: "ript" (从倒数第4位开始截取到末尾)
// 负数结合区间测试
console.log(str.slice(-4, -1)); // 输出: "rip" (等价于 slice(6, 9))
console.log(str.substring(-4, 3)); // 输出: "Jav" (等价于 substring(0, 3))
console.log(str.substr(2, -1)); // 输出: "" (length 为负数,返回空串)
2. 起始索引大于结束索引(参数倒置)
当业务逻辑计算出的 start > end 时:
-
slice严格保持方向性:由于slice必须从左向右读取,当起始位置在结束位置右侧时,它不会进行隐式猜测,直接返回空字符串""。这是一种安全、可预测的行为,能及时暴露上游计算的逻辑问题。 -
substring会自动交换参数:如果start > end,substring会悄悄交换两者的位置,将其视为substring(end, start)。例如str.substring(5, 2)会在底层被自动重写为str.substring(2, 5)并返回"vaS"。这种“宽容”特性在某些时候可能带来便利,但在大型系统中极容易掩盖参数传递的错误。 -
substr不存在倒置概念:因为它的第二参数是长度而非下标,str.substr(5, 2)表示从下标 5 开始取 2 个字符,返回"cr"。
const str = "JavaScript";
console.log(str.slice(5, 2)); // 输出: "" (空字符串,因 start > end)
console.log(str.substring(5, 2)); // 输出: "vaS" (自动对调为 substring(2, 5))
console.log(str.substr(5, 2)); // 输出: "cr" (从位置 5 截取长度 2)
四、弃用现状与 substr 迁移重构策略
在 ECMAScript 规范中,String.prototype.substr 早已被移至 Annex B(规范规范附录 B) 中。附录 B 专门收纳为了兼容远古 Web 浏览器而保留的旧特性,官方在规范中明确标注:“不推荐在新的 ECMAScript 代码中使用本特性”。
substr 虽然目前在绝大多数浏览器中依然能运行,但某些非标准运行时(或者严格安全沙箱环境)可能不会实现该扩展。现代代码审查与 ESLint 通常会配置 no-substr 规则将其标记为警告或错误。
如何安全地将 substr 重构为 slice?
将现有的 str.substr(start, length) 替换为 str.slice() 非常简单,转换核心在于将“长度”换算为“结束下标”:
- 当只有一个参数时:
str.substr(start)与str.slice(start)完全等价,直接无缝替换即可。 - 当
start >= 0且有length时:等价公式为str.slice(start, start + length)。例如str.substr(2, 5)转换为str.slice(2, 2 + 5)即str.slice(2, 7)。 - 当
start < 0时:需要先将负数索引标准化为正数索引,再叠加长度:
/**
* 安全的 substr 到 slice 平滑替换实现
* @param {string} str 原字符串
* @param {number} start 起始位置
* @param {number} [length] 截取长度
*/
function safeSubstrToSlice(str, start, length) {
if (length === undefined) {
return str.slice(start);
}
if (length <= 0) {
return "";
}
// 将负数 start 规范化为从 0 开始的非负下标
const normalizedStart = start < 0 ? Math.max(0, str.length + start) : start;
return str.slice(normalizedStart, normalizedStart + length);
}
// 验证迁移效果
const demo = "JavaScript";
console.log(demo.substr(-6, 4)); // 输出: "Scri"
console.log(safeSubstrToSlice(demo, -6, 4)); // 输出: "Scri"
五、综合选型建议与常见陷阱规避
掌握了三者的区别之后,在日常业务开发和项目架构中应该如何选择?
slice。
之所以推荐统一使用 slice,主要原因如下:
-
统一的 API 习惯:JavaScript 中的
Array.prototype.slice与String.prototype.slice具有几乎一致的参数语义和行为规则。使用slice能大幅降低在数组和字符串之间切换时的心智负担。 -
强大的倒数截取能力:
slice原生支持负数索引,例如获取文件名后缀filename.slice(-3)或截取除最后两位外的字符str.slice(0, -2),代码表达极度简洁优雅,无需手动计算str.length - n。 -
行为严谨可预期:
slice不会像substring那样随意对调参数或将负数静默归零,这能够让潜在的边界传参 Bug 尽早暴露,提高系统的健壮性。
常见避坑要点:
-
获取文件后缀名时误用
substring(-3): 很多开发者误以为substring(-3)能像 Python 负数切片那样拿到后三位,结果却返回了完整的全字符串。正确写法必须使用str.slice(-3)。 -
误将
substr当作区间截取: 如果误把substr(5, 8)当作“从 5 截取到 8”,实际却会截取 8 个字符(直接到了末尾甚至越界),导致截取范围严重超预期。 -
防御性编程时注意空串保护:
当
start超出字符串长度时,slice会返回空字符串""而不会抛出异常,这是安全的,但如果对返回值继续进行链式索引操作,应做好判空防御。
实用工具推荐
绿化、压缩、噪点和虚拟水印,自由调节做旧程度。
选择画质和输出格式,预览压缩前后的大小。