ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

JavaScript字符串截取三兄弟:substr、substring、slice深度解析

JavaScript字符串截取三兄弟:substr、substring、slice深度解析 1. 字符串截取三兄弟为什么一个话题能写几十年很多JavaScript初学者第一次接触字符串处理时都会对着这三个方法发呆substr()、slice()、substring()名字长得像三胞胎功能也几乎一样——都是截取字符串。但真到用的时候却经常因为记错参数含义而出各种诡异的bug。这个话题在技术社区里的讨论热度从来没降过面试题里也总能看到它们的身影因为它背后其实藏着一整套关于JavaScript字符串处理的历史包袱、参数设计和边界行为。简单说这三个方法都能从字符串里“切”出一段子串。但它们的参数规则有明显差异substr(start, length)第二个参数是“截取长度”substring(start, end)第二个参数是“结束索引”且会自动调整大小顺序slice(start, end)第二个参数也是“结束索引”但支持负数从末尾倒数substring和slice的差异主要在负数处理上substr则是完全不同的参数语义。这个知识点虽然基础却是真正的高频坑因为它牵涉到“索引从0开始”“左闭右开区间”“负数是否支持”这几个JavaScript字符串处理的底层规则。我准备用一篇完整的拆解把这三个方法的来龙去脉、参数细节、边界行为、实战选型全部讲清楚。不管你是刚入门的前端新人还是写了好几年业务代码但偶尔还会查文档的老手这篇内容应该都能帮你彻底结束对这三个方法“见一次查一次文档”的状态。本文所有示例都基于JavaScript的String.prototype上的原生方法运行环境为Node.js或现代浏览器结果以控制台输出为准。2. 参数规则拆解三个方法到底差在哪2.1 substr只有它用“长度”作为第二参数substr(start, length)是这三个方法里唯一一个把第二参数定义为“截取长度”的。它的行为可以用一句话概括从start位置开始向右数length个字符。const str JavaScript; console.log(str.substr(0, 4)); // Java console.log(str.substr(4, 6)); // Script console.log(str.substr(4)); // Script // 省略第二参数时默认截取到字符串末尾这里有一个容易忽略的细节substr的start参数支持负数。当start为负数时它会从字符串末尾反向计算起始位置-1表示倒数第一个字符。const str JavaScript; console.log(str.substr(-3, 3)); // ipt // 从倒数第3个字符开始截取3个字符 console.log(str.substr(-6)); // Script // 从倒数第6个字符开始截取到末尾关于length参数有两点值得注意。第一如果length是负数或0返回的是空字符串。第二如果length超出了从start到末尾的实际长度不会报错而是直接截取到字符串末尾为止。这两个行为都很“宽容”但也正因为宽容容易掩盖代码里的逻辑错误。还要明确一点substr在ECMAScript规范中并不是标准方法。它最初来自JavaScript 1.2Netscape时代后来被各大浏览器广泛支持但一直没有进入新版的ECMA-262规范正文。虽然如今所有主流环境浏览器、Node.js都能正常使用它但在标准合规性上它属于“非标准但事实上支持”的方法。在严格追求规范的项目里一些代码规范工具会提示优先使用slice或substring。2.2 substring会自动帮你调整参数顺序substring(start, end)的参数是两个索引返回从start到end之间的字符包含start不包含end。这个“左闭右开”的区间规则和slice是一致的很多不熟悉的人在这里会踩坑。const str JavaScript; console.log(str.substring(0, 4)); // Java // 索引0开始到索引4之前结束 console.log(str.substring(4, 10)); // Script // 索引4开始到索引10之前结束substring最特殊的行为是“参数交换”。如果start大于end它不会按你写的顺序去截取而是先交换两个参数再执行截取。这一点在日常业务中很容易隐藏问题——比如你本意是从后面往前面截结果它直接帮你把顺序纠正了返回的结果反而是你预期的反面。const str JavaScript; console.log(str.substring(4, 0)); // Java // 你以为会得到Script实际得到的是Java // 因为内部把参数调整为 substring(0, 4)另外substring遇到负数参数时会把负数统一当成0处理。也就是说任何负数索引在它面前都被“抹平”成了起点。const str JavaScript; console.log(str.substring(-3, 4)); // Java // -3 被当作 0等价于 substring(0, 4)2.3 slice规则最接近数组的截取方式slice(start, end)的参数同样是两个索引区间规则同样是左闭右开。它与substring最大的区别在于负数处理slice会支持从末尾倒数的负数索引并且不会自动交换参数顺序。const str JavaScript; console.log(str.slice(0, 4)); // Java console.log(str.slice(4, 10)); // Script console.log(str.slice(-6, 10)); // Script // -6 表示倒数第6个字符等价于索引 4由于slice不交换参数所以当start大于end时它会直接返回空字符串而不是像substring那样“好心”帮你改顺序。const str JavaScript; console.log(str.slice(4, 0)); // // start 大于 end直接返回空字符串如果你想要实现“从某位置截到末尾”的效果可以省略第二参数想要从末尾倒数也可以直接传入负数。这些行为规则和数组的Array.prototype.slice完全一致所以熟悉数组操作的人上手slice特别快。3. 三者对比例表一眼看清参数与边界行为为了方便对照我把三个方法的差异整理成了一张速查表。这张表建议收藏遇到不确定的时候直接返回来看。对比项substr(start, length)substring(start, end)slice(start, end)第二参数含义截取长度结束索引不含结束索引不含负数start从末尾倒数当作0处理从末尾倒数负数end不支持负数长度返回空串当作0处理从末尾倒数start end时正常截取从start开始数length个自动交换参数再截取返回空字符串区间规则无索引区间概念按长度计数左闭右开 [start, end)左闭右开 [start, end)省略第二参数截取到末尾截取到末尾截取到末尾标准性非标准但被广泛支持标准ES3标准ES3一眼扫过去最直观的记忆点是substr看长度substring看索引看负数会“软化”slice看索引支持负数但不交换参数。基于这张表我每次在代码里选择方法时基本遵循这个思路我需要“从某位置开始截取固定长度”——用substr最直接我需要“截取从a到b的一段”且双方都确定为正数——用substring我需要“从末尾倒数截取一段”或者参数来自用户输入可能是负数——用slice这个选择思路看起来简单实际操作时能减少很多不必要的判断代码。4. 实战选型什么场景该用哪个方法4.1 固定长度截取场景优先用substr在很多真实业务里我们需要的不是“从a索引到b索引”而是“从某个位置开始截取固定N个字符”。这种场景下substr的参数语义最贴合需求代码读起来也最自然。举几个例子const phone 13812345678; // 取手机号前3位 const prefix phone.substr(0, 3); // 138 // 取手机号后4位 const suffix phone.substr(-4); // 5678身份证号解析、订单号分段、日志格式截取这类场景到处都是“固定长度”的需求。比如从一段日期时间字符串2025-03-15 14:30:00里分别取出日期和时间部分const datetime 2025-03-15 14:30:00; const datePart datetime.substr(0, 10); const timePart datetime.substr(11, 8); console.log(datePart); // 2025-03-15 console.log(timePart); // 14:30:00这里如果非要用substring或slice你还得先计算结束索引起始索引长度代码会多一步计算可读性反而下降。所以我的习惯是明确的按长度截取需求直接用substr简单高效。4.2 按索引区间截取substring和slice怎么选当需求变成“从索引a到索引b之间”的区间截取时substring和slice才成为候选。二者之间我更推荐默认使用slice。理由有三个第一slice行为更可预测。因为substring会自动交换参数当你的start和end来自动态变量时一旦数据异常比如start算大了substring不会暴露问题而是“悄悄地”返回一个你未必想要的结果这种隐藏行为在调试时非常迷惑。slice则不同参数顺序错了就返回空字符串问题暴露得很明显反而更容易追查。第二slice支持负数比如从末尾往前截取的场景用slice可以写出很简洁的代码const filePath /usr/local/nginx/logs/access.log; // 取最后两级路径 const lastTwo filePath.slice(-9); // access.log // 去掉后缀名 const nameWithoutExt filePath.slice(0, -4); // .../access第三slice的语义和数组方法完全一致。如果你也经常操作数组那slice在字符串和数组两边通用一套规则记忆成本更低。4.3 关于substr“非标准”问题的实际影响前面提到substr不是ECMAScript标准方法有些读者可能担心在项目里用substr会不会有兼容性或合规性风险从实际角度讲所有现代JavaScript运行环境包括浏览器和Node.js都实现了substr并且没有任何主流环境表示要移除它。ECMA-262虽然没把它写进标准正文但它一直在规范附注Annex B里被列为“浏览器兼容性扩展”所以它并不是“随时可能消失”的野路子API。Web上存量代码太多了彻底移除法子方法会造成大规模兼容性灾难。不过在两种情况下我会刻意避开substr项目启用了严格的ESLint规则集比如eslint:recommended配合某些插件其中no-restricted-properties可能禁用了它团队里有代码规范明确要求只用标准方法这时切换到slice通常只需要改一行因为substr(start, length)在绝大多数场景可以改写为slice(start, start length)const str hello world; // 等价写法 str.substr(6, 5); str.slice(6, 6 5); // 都返回 world如果只是个人项目或非严格规范约束的业务代码substr放心用就好。5. 最容易踩的坑索引边界与参数记忆5.1 左闭右开区间带来的偏一位问题substring和slice都遵循“包含起始索引、不包含结束索引”的规则。这个规则经常让人在计算索引时偏一位。比如一个长度为10的字符串你想截取前5个字符正确写法是const str 0123456789; str.slice(0, 5); // 01234不包含索引5的字符很多人会写成slice(0, 6)多截了一个字符。我的习惯是先想清楚“我要的最后一个字符的索引是多少”然后加1就是end参数。比如我想截取索引2到索引7之间的字符最后一个是索引7那么end参数就是8写成slice(2, 8)。用这个“末尾索引1”的口诀基本不会出现偏一位的问题。5.2 负数索引在三种方法中的表现截然不同这是最容易踩坑的地方因为三个方法对负数的处理完全不一样substr的start支持负数length不支持负数substring的所有负数参数都被当作0slice的start和end都支持负数用同一个示例字符串来对比一下感受会更直观const str ABCDE; console.log(str.substr(-2, 2)); // DE console.log(str.substring(-2, 2)); // AB // 注意-2被当成了0截取的是索引0到2的内容 console.log(str.slice(-2, 4)); // D // 从倒数第2个字符(索引3)到索引4之前只有索引3一个字符substring把负数当0这个行为特别容易在“从后往前取”的时候给你一个“看似正确但实际错误”的结果。我早年写过一个解析文件名的函数用substring处理一个带负数的动态索引结果返回的始终是前几个字符排查了很久才发现负数全被归零了。从那之后凡是索引可能为负的场景我直接放弃substring。5.3 参数大小关系导致的行为差异substring和slice在start end时的处理截然不同const str ABCDEF; console.log(str.substring(4, 2)); // CD // 自动交换为 substring(2, 4) console.log(str.slice(4, 2)); // // 直接返回空字符串如果没有意识到substring会交换参数那么在写依赖start end逻辑的代码时很可能会得到反直觉的结果。比如你想从末尾往前截一段字符串用substring写了substring(4, 2)你以为会得到从后往前的内容实际上它返回的是正向区间的内容整个逻辑就错了。所以我个人的项目规范里有一条索引顺序取决于业务逻辑时统一用slice只有确定了start end才允许用substring。6. 那些容易混淆的“亲戚”splice、split以及跨语言截取搜索相关热词时我注意到一个高频现象很多人把splice和slice搞混。虽然splice是数组方法而不是字符串方法但它的存在确实给新手带来了极大的困惑这里必须单独拎出来说清楚。6.1 splice和slice一字之差天壤之别splice是Array.prototype上的方法它的作用是“删除或替换数组元素”而且直接修改原数组。slice则是“复制数组中一段元素返回新数组”并不会改变原数组。const arr [a, b, c, d]; // splice从索引1开始删2个元素并插入x const spliced arr.splice(1, 2, x); console.log(arr); // [a, x, d] console.log(spliced); // [b, c] // slice复制索引1到3之前的元素 const sliced arr.slice(1, 3); console.log(arr); // 原数组保持不变 console.log(sliced); // 从新数组视角看arr 已经是 [a,x,d] 了所以这里结果需要往前对齐splice影响原数组slice不影响这是二者最核心的分界线。如果你在做数据操作时误把splice当成字符串方法调用或者把字符串的slice行为套到数组上就会遇到“原数据被改了”或者“截取结果不对”的问题。这也是为什么相关话题下总是有大量“splice和slice区别”搜索的原因。顺带一提如果你需要字符串的“把某一段替换掉”能力应该用replace而不是splice因为字符串是原始类型本身就没有splice方法。6.2 split也是截取它是“分割”不是“截取”还有一个经常和这三个方法放在一起提的是split。split的职责是用分隔符把字符串拆成数组和“按索引截取”完全是两个维度的事。const str apple,banana,orange; const arr str.split(,); // [apple, banana, orange]split的场景是“把有规律的字符串拆成多个部分”比如解析CSV、处理URL参数、按换行拆多行文本。它不是拿来做“从第几个字符到第几个字符”的。如果把split和substring搞混典型的错误会是这样const str hello; str.split(1, 3); // 返回 [hello]整个字符串作为单个元素 // 你期望的是el但split根本不按索引工作这类混淆的根源其实还是对“按索引截取”和“按分隔符分割”这两类需求的分野不够清晰。按位置截取选本文的三个方法按规则拆分选split。6.3 跨语言视角C#、MATLAB怎么截字符串热搜词里出现了C#和MATLAB的字符串截取说明很多人在不同语言之间切换时会对“截取字符串”的规则感到混乱。这里简单对照一下方便有跨语言需求的读者建立全局认知。C#中常用的是Substring方法它和JavaScript的substr有点类似但它不支持负数string str Hello World; string sub str.Substring(6, 5); Console.WriteLine(sub); // 输出 WorldC#里实现“截取到末尾”只需要传入起始索引string sub2 str.Substring(6); // World而C#里如果要从末尾倒数的位置截取需要先用Length减去偏移量string sub3 str.Substring(str.Length - 5); // WorldMATLAB中常见的是extractBetween或substringMATLAB 2016b及以后版本支持substring但版本之间行为不一致。MATLAB的字符串索引从1开始这又和JavaScript的从0开始不同跨语言编码时非常容易出边界错误str Hello World; sub extractBetween(str, 1, 5); % 输出 Hello不同语言的索引起点、是否包含结束位置、是否支持负数这三个维度排列组合就是“字符串截取”话题在跨语言场景下总有新文章出现的根本原因。理解了这三条轴之后换语言时只需要对照查一下对应语言属于哪种组合基本就不会再犯错了。7. 常见问题速查三个疑难场景排查实录7.1 为什么substring(4, 0)返回的不是“从4到末尾”这是搜索热词下最常见的问题之一。原因是substring会自动交换参数substring(4, 0)等价于substring(0, 4)。如果你期望“从4到末尾”应该写成substring(4)显式省略第二参数。或者换成slice(4)语义更直白。这个行为在官方文档里写得很清楚但遇到实际代码时真的容易被忽略。7.2 为什么slice(-3, -1)只截到倒数第二个字符因为slice的左闭右开区间规则对负数同样生效。-3表示倒数第3个字符-1作为结束位置表示“到倒数第一个字符之前停止”所以结果不包含最后一个字符。const str abcdef; console.log(str.slice(-3, -1)); // de如果你想要最后3个字符slice(-3)就够了不用提供第二个参数。这类“末尾少一位”的问题往往是没搞清楚“结束索引不包含”在负数下也适用。7.3 substr在处理长度超界时会发生什么当substr的length参数大于从start到末尾的实际长度时它不会报错而是自动截到末尾。比如const str hello; console.log(str.substr(3, 10)); // lo // 虽然给的长度是10但实际只剩2个字符这正是我之前说的“宽容型行为”。它会掩盖一些逻辑上的错误比如你预期字符串一定有足够长度但实际数据并不满足结果代码不报错但输出却不完整。最稳妥的做法是截取之前显式校验长度或者改用slice配合Math.min来明确控制边界。在实际开发中我建议对“截取结果不符合预期”的问题第一时间打印三个东西原始字符串、起始索引、截取参数。至少有一半的问题打印完这三个值之后自己就能定位了。7.4 动态截取时怎么避免“隐藏问题”实际开发里截取参数往往来自变量而不是字面量。为了避免活活踩坑我通常会在截取前做一次边界判断function safeSlice(str, start, end) { const len str.length; const safeStart Math.max(0, Math.min(start, len)); const safeEnd end undefined ? len : Math.max(safeStart, Math.min(end, len)); return str.slice(safeStart, safeEnd); }这段保护代码的思路是把start限制在[0, len]范围内把end限制在[safeStart, len]范围内保证永远不会因为负数和越界而导致意外结果。虽然多了几行但在处理用户输入或后端传输的数据时这个前置校验值回票价。8. 个人使用规范我最终沉淀下来的几条习惯聊了这么多规则和案例最后分享一下我在项目里实际沉淀下来的几条使用习惯。这些习惯不一定是唯一正确解但它们是经过多次返工和bug排查之后我最终固定的方案。第一默认入口是slice。无论是字符串还是数组slice的行为最可预测支持负数不交换参数兼容新老环境风格统一。我在自己的代码规范里凡是需要“按索引截取”的无特殊理由都用slice。第二按长度截取才用substr。只有业务需求明确是“从第几位开始取多少位”时substr的代码可读性优势才值得用。而且我会在注释里写清楚“这里按长度截取”避免同事误以为是索引区间。第三一律不用substring做动态参数的处理。除非参数来源是已验证过的固定数字比如从配置表里读出来的常量我才允许用substring。凡是有变量参与、索引可能来自计算结果的substring的自动交换和负数归零行为都是隐患。第四统一封装一个工具函数。在一个中大型项目里多处用到的字符串截取逻辑最好收敛到一个公共模块里而不是到处直接写slice或substr。这样当需求变更比如统一改用正则匹配或统一处理中文双字节时只改一个地方就够了。我在实际项目中见过太多次因为直接散写截取逻辑导致后期难以维护的情况。字符串截取虽然是一个小知识点但它在业务代码中的出现频率极高值得从一开始就规范起来。其实这三个方法本身并不难难的是在真实代码里形成稳定的判断力和使用习惯。希望这篇拆解能帮你少走一些弯路下次见到它们的时候不再是“查一次忘一次”的状态。
返回列表