ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

JavaScript字符串截取方法详解:slice、substr、substring、split对比与避坑指南

JavaScript字符串截取方法详解:slice、substr、substring、split对比与避坑指南 字符串处理是每个开发者绕不开的基本功而截取操作又是其中最高频的需求之一。不管你是刚接触 JavaScript 的新手还是写了几年业务代码的老手slice、substr、substring、split这四个方法几乎每天都会出现在你的代码里。但说实话我见过太多项目里这四个方法用混的情况——有人拿substr当substring用有人用split切完字符串忘了处理空值还有人压根不清楚负数参数在不同方法里的行为差异。这些细节平时不出问题还好一旦碰到边界条件就是线上 bug。这篇文章我会把这四个方法的参数规则、边界行为、适用场景和踩坑经验全部拆开讲清楚每个方法都会配上可直接运行的代码示例和参数计算过程不管你是刚入门还是想查漏补缺都能拿到可以直接用的结论。1. 四个方法的核心定位与设计思路拆解1.1 为什么字符串截取需要四个方法刚学 JavaScript 的时候我也有过这个疑问截取字符串而已搞这么多方法干什么后来代码写多了才明白这四个方法各自解决的是不同维度的问题。slice和substring解决的是从位置 A 到位置 B 取一段的问题但两者对负数参数的处理策略完全不同。substr解决的是从位置 A 开始取 N 个字符的问题它的第二个参数是长度而不是结束位置这个设计思路跟另外两个有本质区别。split则完全是另一个维度——它不是按位置截取而是按分隔符把字符串拆成数组。你可以这样理解前三个是剪刀按照坐标剪split是切菜刀按照纹路切。剪刀适合你知道确切位置的情况切菜刀适合你知道分隔规则的情况。从语言设计的历史来看substring是最早出现的它的行为设计偏向安全——负数参数一律当 0 处理参数顺序反了会自动交换。slice后来加入设计上更激进也更一致——支持负数索引行为跟数组的slice保持统一。substr是从其他语言借鉴过来的第二个参数是长度这个设计在 Perl 等语言里很常见但它现在已经被标记为废弃deprecated了虽然浏览器还支持但新项目不建议再用。1.2 四个方法的核心参数对比先把四个方法的基本签名和参数含义列出来这是后面所有讨论的基础方法签名第一个参数第二个参数返回值slicestr.slice(start, end)起始索引支持负数结束索引不含该位置支持负数省略则到末尾新字符串substringstr.substring(start, end)起始索引负数当0结束索引不含该位置负数当0省略则到末尾新字符串substrstr.substr(start, length)起始索引支持负数截取长度负数当0省略则到末尾新字符串splitstr.split(separator, limit)分隔符字符串或正则限制返回数组的最大长度字符串数组这张表建议你记牢尤其是第二个参数的含义差异——slice和substring的第二个参数是结束位置substr的第二个参数是长度。这是最容易搞混的地方我自己就曾经在紧急改 bug 的时候把substr(2, 5)当成从索引2到索引5来用结果多截了三个字符排查了半天才发现问题。1.3 选型决策什么场景用哪个方法基于我自己的使用经验给你一个简单的决策路径知道起始和结束位置且可能涉及从末尾倒数 → 用slice知道起始和结束位置参数都是正数且希望容错参数写反了自动纠正 → 用substring知道起始位置和要取多少个字符 → 用substr但建议尽量改用slice替代需要按某个字符或模式把字符串拆成多段 → 用split实际项目中slice和split的使用频率远高于另外两个。substring偶尔在不需要负数支持的场景下出现substr基本只在维护老代码时才会遇到。2. slice 方法深度解析与实操要点2.1 slice 的参数规则与负数索引计算slice是我个人最推荐使用的截取方法原因很简单它的行为最一致跟数组的slice完全对齐支持负数索引参数含义清晰。它的规则是这样的str.slice(start, end)返回从start到end不含end之间的字符。如果省略end就截到字符串末尾。关键点在于负数索引的处理——负数表示从字符串末尾往前数-1是最后一个字符-2是倒数第二个以此类推。这里有一个容易出错的细节当负数索引的绝对值超过字符串长度时会被截断为 0。比如字符串长度是 5你传-10实际会被当作 0 处理。这个行为跟 Python 的切片类似但跟某些其他语言不同需要留意。我整理了一个参数计算的对照表方便你快速查阅字符串调用实际起始实际结束结果helloslice(1, 3)13elhelloslice(-3, -1)24llhelloslice(2)25llohelloslice(-2)35lohelloslice(3, 1)31空字符串helloslice(-100, 2)02he注意倒数第二行当start大于end时slice返回空字符串不会自动交换参数。这一点跟substring不同后面会对比。2.2 slice 的典型应用场景slice最常用的场景有这么几个场景一去掉字符串首尾的固定字符。比如你有一个带前缀的 IDUSER_12345想拿到纯数字部分可以这样写const id USER_12345; const num id.slice(5); // 12345场景二获取文件扩展名。利用负数索引从末尾定位const filename report.2024.pdf; const ext filename.slice(filename.lastIndexOf(.) 1); // pdf场景三截取末尾固定长度的字符。比如取手机号后四位const phone 13812345678; const last4 phone.slice(-4); // 5678场景四复制整个字符串。slice()不传参数会返回原字符串的副本这个用法在某些需要确保不修改原值的场景下很有用const original hello; const copy original.slice(); // hello2.3 slice 的注意事项与踩坑记录注意slice不会修改原字符串。JavaScript 中字符串是不可变的所有截取方法返回的都是新字符串。我踩过的一个坑是以为slice的负数索引在start end时会自动调整。实际上不会。比如hello.slice(-1, -3)返回的是空字符串因为实际起始是 4结束是 2起始大于结束。如果你需要从倒数第一个到倒数第三个这种反向截取得自己处理。另一个坑是关于undefined参数的。slice(undefined, 3)等价于slice(0, 3)因为undefined会被转成 0。但如果你从某个可能返回undefined的变量取值传进去就容易出意外。建议在传参前做好类型检查。还有一个性能相关的经验对于超长字符串的截取slice的性能表现是四个方法里最好的之一因为它不需要像split那样创建数组。如果你在循环里做大量截取操作优先用slice。3. substring 方法深度解析与实操要点3.1 substring 的参数规则与自动交换机制substring的行为跟slice很像但有两个关键区别第一它不支持负数索引所有负数参数都会被当作 0第二如果start大于end它会自动交换两个参数。这个自动交换的机制说实话我觉得是把双刃剑。好处是容错性强参数写反了不会返回空字符串。坏处是它掩盖了逻辑错误——你可能本来想写slice结果写成了substring参数顺序错了但代码还能跑等到某天参数恰好是正序的时候就出 bug 了。来看参数对照字符串调用实际起始实际结束结果hellosubstring(1, 3)13elhellosubstring(3, 1)13el自动交换hellosubstring(-3, 2)02he负数当0hellosubstring(2)25llohellosubstring(-1, -3)00都当0注意最后一行两个负数都变成 0所以起始和结束都是 0返回空字符串。这个行为跟slice完全不同slice(-1, -3)至少起始是 4虽然结果也是空字符串但原因不一样。3.2 substring 的适用场景与替代方案substring最适合的场景是你确定参数都是非负整数且希望代码有一定的容错性。比如从固定格式的字符串里按位置取值const dateStr 2024-01-15; const year dateStr.substring(0, 4); // 2024 const month dateStr.substring(5, 7); // 01 const day dateStr.substring(8, 10); // 15这种场景下用substring和slice效果一样但substring的自动交换机制在参数计算可能出错时能兜底。不过我现在更倾向于统一用slice原因是第一slice支持负数功能更全第二slice的行为跟数组方法一致减少认知负担第三substring的自动交换有时候会掩盖 bug。如果你团队里没有强制规范我建议新代码统一用slice。3.3 substring 的常见误区注意substring的负数参数不是从末尾倒数而是直接当 0。这一点跟slice有本质区别千万别混用。我见过一个真实的 bug开发者想取字符串最后三个字符写了str.substring(-3)以为跟slice(-3)一样。结果substring把-3当 0返回了整个字符串。这个 bug 在测试环境没被发现因为测试数据的字符串恰好很短上线后遇到长字符串才暴露出来。另一个误区是认为substring和slice可以互换。在参数都是正数且start end的情况下两者确实等价。但一旦涉及负数或参数顺序问题行为就分道扬镳了。我的建议是要么统一用slice要么统一用substring不要在同一个项目里混着用否则维护成本会很高。4. substr 方法深度解析与实操要点4.1 substr 的参数规则与长度语义substr的第二个参数是长度这是它跟另外两个方法最本质的区别。str.substr(start, length)表示从start位置开始取length个字符。它的第一个参数支持负数跟slice一样从末尾倒数但第二个参数如果是负数会被当作 0返回空字符串。字符串调用实际起始截取长度结果hellosubstr(1, 3)13ellhellosubstr(-3, 2)22llhellosubstr(2)2到末尾llohellosubstr(1, -2)10hellosubstr(-100, 3)03hel注意第一行substr(1, 3)返回ell而slice(1, 3)返回el。这就是长度和结束位置的区别差一个字符。4.2 substr 的废弃状态与迁移建议substr目前在 ECMAScript 规范里已经被标记为废弃Annex B虽然所有主流浏览器还支持但新项目不应该再使用。废弃的原因主要是它的长度语义跟其他字符串方法不一致容易造成混淆。如果你在维护老代码遇到substr需要迁移到slice转换规则是这样的// 原代码 str.substr(start, length) // 等价改写 str.slice(start, start length)但要注意边界情况当start是负数时slice的负数语义跟substr一致都是从末尾倒数所以这个转换是安全的。当length省略时slice(start)也等价于substr(start)。我实际迁移过一个中型项目里的substr调用大概有三十多处用上面的规则批量替换后跑了一遍单元测试全部通过。唯一需要注意的是有些地方length是变量要确保start length的计算不会溢出——不过对于字符串索引来说超出部分会自动截断所以问题不大。4.3 substr 的遗留代码处理经验注意虽然substr还能用但如果你在做代码审查看到新代码里出现substr建议直接要求改成slice。我在实际项目中处理substr的经验是不要一次性全部替换而是按模块逐步迁移。因为有些老代码可能依赖了substr的某些边界行为比如负数长度的处理批量替换可能引入意外。稳妥的做法是先加测试用例覆盖边界情况再替换再跑测试验证。另外如果你用的是 TypeScriptsubstr在lib.es5.d.ts里还有类型定义但一些 lint 规则比如eslint的no-restricted-properties可以配置禁止使用substr建议在项目里开启这个规则从源头杜绝新代码使用废弃方法。5. split 方法深度解析与实操要点5.1 split 的核心机制与分隔符类型split跟前面三个方法完全不是一个路数。它不是按位置截取而是按分隔符把字符串拆成数组。str.split(separator, limit)的第一个参数是分隔符可以是字符串也可以是正则表达式第二个参数限制返回数组的最大长度。const str apple,banana,orange; const arr str.split(,); // [apple, banana, orange]用正则作为分隔符是split的强大之处。比如你想按任意空白字符分割const text hello world\tfoo\nbar; const words text.split(/\s/); // [hello, world, foo, bar]这里\s匹配一个或多个空白字符包括空格、制表符、换行符。如果不加连续的空格会产生空字符串元素。5.2 split 的边界行为与空字符串处理split有几个容易踩坑的边界行为我逐个说空分隔符hello.split()返回[h, e, l, l, o]每个字符单独拆开。这个用法在需要逐字符处理时很有用但要注意它会把代理对比如 emoji拆坏。对于包含 emoji 的字符串应该用Array.from(str)或扩展运算符[...str]。分隔符在开头或结尾,a,b,.split(,)返回[, a, b, ]首尾的空字符串会保留。如果你不想要空字符串需要额外过滤const parts ,a,b,.split(,).filter(Boolean); // [a, b]分隔符不存在hello.split(,)返回[hello]包含原字符串的单元素数组。limit 参数a,b,c,d.split(,, 2)返回[a, b]只取前两个。注意limit限制的是数组长度不是分割次数。调用结果说明a,b,c.split(,)[a,b,c]常规分割a,b,c.split(,, 2)[a,b]限制长度abc.split()[a,b,c]空分隔符.split(,)[]空字符串分割a,,b.split(,)[a,,b]保留空元素a1b2c.split(/\d/)[a,b,c]正则分割5.3 split 与其他方法的组合使用split单独用的场景其实不多更多时候是跟其他方法组合。比如经典的取 URL 参数const url https://example.com?namefooage18; const query url.split(?)[1]; // namefooage18 const params {}; query.split().forEach(pair { const [key, value] pair.split(); params[key] value; }); // { name: foo, age: 18 }再比如反转字符串这个经典面试题用splitreversejoin三连const reversed hello.split().reverse().join(); // olleh还有统计字符出现次数const str hello; const count {}; str.split().forEach(char { count[char] (count[char] || 0) 1; }); // { h: 1, e: 1, l: 2, o: 1 }5.4 split 的性能考量与注意事项注意split会创建新数组对于超长字符串和高频调用场景性能开销比slice大。如果只是判断是否包含某个子串用includes或indexOf更合适。我做过一个简单的性能测试对一个 10 万字符的字符串做 1 万次操作slice大概比split快 3 到 5 倍。当然这个数据跟具体场景有关但结论是明确的能用slice解决的不要用split。另一个坑是正则分隔符的贪婪匹配。比如a1b22c.split(/\d/)返回[a,b,c]因为\d把连续的22当作一个分隔符。如果你想要每个数字都作为分隔符应该用\d而不是\d。还有一点split的分隔符如果是正则且包含捕获组捕获组的内容也会出现在结果数组里。这个行为很多人不知道a1b2c.split(/(\d)/); // [a, 1, b, 2, c]如果你不需要捕获组内容用非捕获组(?:\d)就行。6. 四个方法的对比实战与常见问题排查6.1 同一需求用四种方法实现的对比用一个具体需求来对比四个方法的差异从2024-01-15中取出月份01。const date 2024-01-15; // 方法1slice const m1 date.slice(5, 7); // 01 // 方法2substring const m2 date.substring(5, 7); // 01 // 方法3substr const m3 date.substr(5, 2); // 01 // 方法4split const m4 date.split(-)[1]; // 01四种方法都能实现但适用场景不同。前三种需要你知道月份在字符串中的确切位置第四种只需要你知道分隔符。如果日期格式可能变化比如2024-1-15前三种方法就会出错而split依然能正确工作。这就是为什么理解每个方法的适用场景比记住语法更重要。6.2 常见问题速查表我把实际开发和代码审查中遇到的高频问题整理成了一张表问题现象可能原因排查方法解决方案截取结果多/少一个字符substr的第二个参数是长度不是结束位置检查方法名和参数含义改用slice或调整参数负数参数返回整个字符串用了substring而非slice确认方法是否支持负数改用slice参数顺序写反返回空字符串slice不自动交换参数检查 start 和 end 大小改用substring或手动调整split结果包含空字符串分隔符在首尾或连续出现打印原始字符串检查用filter(Boolean)过滤emoji 被拆坏用空字符串split拆分了代理对检查字符串是否含 emoji改用Array.from或[...str]正则分割结果多出元素正则包含捕获组检查正则是否有()改用非捕获组(?:)6.3 独家避坑技巧与经验总结分享几个我在实际项目中总结的技巧技巧一统一用slice作为默认截取方法。除非有明确理由用其他方法否则slice是最安全的选择。它支持负数行为一致性能好跟数组方法对齐。团队里统一用slice能减少大量沟通成本。技巧二split之后永远考虑是否需要过滤空值。尤其是处理用户输入或外部数据时首尾分隔符、连续分隔符都很常见。养成split后接filter(Boolean)的习惯能避免很多边界 bug。技巧三用indexOfslice替代split取单段。如果你只需要取分隔后的某一段不需要完整数组用indexOf找到位置再slice性能更好// 不推荐创建了完整数组 const name keyvalue.split()[1]; // 推荐只做一次查找和截取 const str keyvalue; const idx str.indexOf(); const name2 idx 0 ? str.slice(idx 1) : ;技巧四处理多字节字符时避开空字符串split。前面提过.split()会返回两个乱码字符。正确做法是[...()]或Array.from()它们会按码点拆分而不是按 UTF-16 码元。技巧五substr迁移到slice时注意length为undefined的情况。substr(2, undefined)等价于substr(2)截到末尾。但slice(2, 2 undefined)会变成slice(2, NaN)返回空字符串。迁移时要处理这种情况// 安全的迁移写法 const result length undefined ? str.slice(start) : str.slice(start, start length);这些技巧看起来都是小细节但正是这些细节决定了代码在边界条件下是否可靠。字符串截取这种基础操作写对了不出彩写错了就是线上事故。希望这些经验能帮你少走一些弯路。
返回列表