深入对比JavaScript中slice、substring与substr三种截取字符串方法的核心区别,从参数定义、负数索引映射、参数顺序颠倒处理、ECMAScript规范弃用状态及工程最佳实践进行系统解析。
JS中slice、substring和substr截取字符串的区别
时间:2026-11-05
浏览:485
文章目录
slice()、substring() 和 substr() 都可以用来从原字符串中提取一段子字符串并返回新字符串,但它们的参数语义、负数处理方式、起止颠倒容错以及ECMAScript 规范状态存在根本差异:slice(start, end) 与 substring(start, end) 的第二个参数都是结束索引(左闭右开),而 substr(start, length) 的第二个参数是字符长度;slice 完整支持负数倒数索引,substring 会把所有负数强转为 0,而 substr 已被 ECMAScript 官方弃用(移入 Annex B 遗留特性)。现代前端开发中应一律以 slice() 作为首选。
一、核心区别概览与参数模型
为了理清这三个方法,首先需要明确它们的参数签名与数学区间定义。JavaScript 中字符串索引从 0 开始计算,且原字符串具有不可变性(Immutable),任何截取操作均不会改变原字符串,而是分配并返回一个全新的字符串。
| 方法签名 | 第二个参数含义 | 区间特征 | 负数索引支持 | 起点大于终点时 (start > end) | 规范状态与建议 |
|---|---|---|---|---|---|
str.slice(start, end) |
结束位置索引(endIndex) | 左闭右开 [start, end) |
完整支持(len + index 倒数) |
不交换,直接返回空串 "" |
现代首选推荐 与 Array 统一 |
str.substring(start, end) |
结束位置索引(endIndex) | 自动排序区间 [min, max) |
不支持(负数与 NaN 一律当 0) | 自动颠倒调换两参数顺序 | 标准保留 仅 String 具备 |
str.substr(start, length) |
截取字符长度(count) | 从起点向后计数 length 个字符 |
起点支持倒数;长度负数归 0 | 不存在起止颠倒(参数为长度) | 已废弃 (Annex B) 严禁在新项目使用 |
从底层参数设计来看:
slice(start, end):提取从start索引开始到end索引(不包含end本身)的字符。当第二个参数缺省时,默认截取到字符串末尾。substring(start, end):同样是基于起止索引提取字符,但它带有极强的“宽容式容错机制”——如果传入的start > end,底层算法会自动将两者的值调换。substr(start, length):设计初衷是以“起始位置 + 字符个数”的方式工作。由于与其他语言及 JS 自身的Array.prototype.slice命名规则易发生混淆,且在处理跨平台负数参数时历史实现不一,已于早年被标准委员会移出核心规范。
二、负数索引的处理逻辑与推导流程
在日常业务中,最常引发意外 bug 的就是负数参数。例如,开发者想要“截取掉末尾的两个字符”或“提取最后四位字符”时,三种方法的反应截然不同。
const str = "ECMAScript"; // str.length = 10
// 1. slice: 遇到负数,自动加上字符串长度 (10 + (-6) = 4, 10 + (-2) = 8)
console.log(str.slice(-6, -2)); // 输出: "aScr" (索引 4 到 8)
console.log(str.slice(-4)); // 输出: "ript" (从末尾倒数 4 个截取到结束)
// 2. substring: 不支持负数,所有负数直接归 0!
console.log(str.substring(-6, 4)); // 等价于 str.substring(0, 4) -> 输出: "ECMA"
console.log(str.substring(-6, -2)); // 等价于 str.substring(0, 0) -> 输出: ""
// 3. substr: 起点支持负数 (10 + (-4) = 6),但长度参数不能为负数
console.log(str.substr(-4, 3)); // 从倒数第 4 个开始截取 3 个字符 -> 输出: "rip"
console.log(str.substr(2, -1)); // 长度为负数,按 0 处理 -> 输出: ""
ECMAScript 规范(ECMA-262)对参数的标准化执行步骤清晰地解释了这种差异:
- 类型转换:所有参数均先通过
ToIntegerOrInfinity转换为整数。如果是NaN、null或未提供数值,会被视为0; - 负数归一化:对于
slice,若index < 0,则将坐标重定为Math.max(len + index, 0);而对于substring,任何index < 0的参数直接被置为0; - 边界防溢出:所有计算后的索引均通过
Math.min(index, len)钳制在字符串长度以内,保证内存安全。
substring 传递负数!许多开发者误以为 str.substring(-4) 能获取末尾字符,实际上 -4 被静默转化为 0,导致返回了整段字符串,产生致命的业务逻辑缺陷。
三、起始索引大于结束索引时的特殊行为
另一个显著区别在于当传入的第一个参数数值大于第二个参数数值时,方法如何处理。以下通过一段具体包含标点符号和空格的字符串 "Hello, World!" 进行实测对照:
const message = "Hello, World!"; // length = 13
// 场景:start = 7, end = 2
// 1. slice 严格按照单调递增区间处理,7 > 2,左边界已超过右边界,直接返回空串
console.log(message.slice(7, 2)); // 输出: ""
// 2. substring 发现 start > end 时,内部自动调用 swap 翻转参数为 (2, 7)
console.log(message.substring(7, 2)); // 等价于 message.substring(2, 7) -> 输出: "llo, "
// 3. substr 第二个参数是长度,从 7 开始向后读取 2 个字符
console.log(message.substr(7, 2)); // 输出: "Wo"
substring() 的这一设计来自早期的容错理念:在文本选区或者鼠标拖拽框选等交互场景中,用户可能从右向左选中文字,此时起始坐标可能大于终点坐标,substring 会自动纠正次序,保证一定能截取到两个坐标之间的字符。
然而在严谨的编程与算法逻辑中,slice() 严格不翻转的行为更为安全。如果由于参数计算错误导致起点跑到了终点后面,slice() 返回空字符串能直接暴露预期不符,而 substring() 自动翻转反而会掩盖坐标计算的逻辑 bug。
四、规范现状:为什么应当彻底废弃 substr
在 ECMAScript 规范的发展历程中,String.prototype.substr() 早已被正式列为遗留特性(Legacy Features)。
String.prototype.substr 仅为了兼容老旧网页的遗留脚本而存在。规范明确建议“所有新的 ECMAScript 代码均不应使用或依赖此属性中的任何特性”。
彻底弃用 substr() 的技术原因包括:
- 历史跨浏览器兼容性缺陷:在早期的 IE 浏览器(IE8 及更早版本)中,
substr()的第一个参数根本不支持负数索引;传递负数会被错误地当作0处理。这一历史包袱使得其跨环境表现极其脆弱; - 概念不一致:现代 JavaScript 集合类(包括
Array、TypedArray以及String)均以(start, end)作为切片抽象接口,唯独substr是(start, length),增加了团队协作的心智切换成本; - TS/ESLint 规则预警:各大主流 ESLint 规则集(例如
eslint-plugin-unicorn的prefer-string-slice)均会将substr标记为警告或错误,自动化工具会自动提示重构。
无损平滑迁移方案
如果在维护历史存量代码时遇到了 substr,可以用 slice() 按如下数学等价关系进行无痛替换:
// 原写法的基本形式:从 start 开始截取 length 个字符
// 替换公式:str.slice(start, start + length)
// 示例 1:正数索引
str.substr(3, 5); // 替换为:
str.slice(3, 3 + 5); // 即 str.slice(3, 8)
// 示例 2:仅有起始位置,截取至末尾
str.substr(4); // 替换为:
str.slice(4);
// 示例 3:负数起始位置,截取至末尾
str.substr(-4); // 替换为:
str.slice(-4);
// 示例 4:负数起始位置,且指定长度
str.substr(-6, 4); // 替换为:
str.slice(-6, -6 + 4); // 即 str.slice(-6, -2)
五、工程实战场景与选型指南
在实际企业级开发中,应当如何选择最合适的字符串截取方式?结论非常清晰:在 99% 的场景下,统一使用 slice()。
- 心智模型高度统一:JavaScript 中的
Array.prototype.slice和String.prototype.slice规则 100% 一致。无论处理数组切片还是字符串切片,完全复用同一套参数认知; - 优雅处理倒数需求:提取文件后缀扩展名只需
filename.slice(-3),无需额外编写冗长且易错的filename.substring(filename.length - 3); - 行为可预测性高:严格遵循
[start, end)数学区间,越界与逆序均安全返回空串,不产生副作用。
常见业务场景代码模式
// 场景 1:获取文件名扩展名
const getFileExt = (filename) => {
const dotIdx = filename.lastIndexOf(".");
return dotIdx === -1 ? "" : filename.slice(dotIdx + 1);
};
// 场景 2:手机号/敏感信息脱敏 (隐藏中间 4 位)
const maskPhone = (phone) => {
if (!phone || phone.length < 11) return phone;
return phone.slice(0, 3) + "****" + phone.slice(-4);
};
// 场景 3:裁剪过长文本添加省略号
const truncateText = (text, maxLength) => {
return text.length > maxLength ? text.slice(0, maxLength) + "..." : text;
};
六、避坑自查清单
- 不要把负数传给
substring():如果代码中有类似str.substring(str.indexOf(x), -1)的写法,负数会变成0并触发参数翻转,实际执行等同于str.substring(0, str.indexOf(x)),往往产生与预期完全相反的结果。 - 注意数组与字符串方法的差异:
Array实例有slice(),但没有substring()和substr()。如果误在数组对象上调用arr.substring()会直接抛出TypeError: arr.substring is not a function。 - 多字节 Emoji 与 Unicode 代理对:无论是
slice还是substring,内部都是按 UTF-16 代码单元(Code Units)截取的。如果遇到像"😀"这种占用两个代码单元(Surrogate Pairs)的 Emoji,直接截取中间可能产生乱码(孤立代理项)。对于可能包含特殊 Unicode 的场景,应优先将字符串展开为字符数组[...str].slice(...)后再进行提取。
实用工具推荐
绿化、压缩、噪点和虚拟水印,自由调节做旧程度。
选择画质和输出格式,预览压缩前后的大小。