JavaScript字符串截取与分割:slice、substring、split核心方法与Unicode处理

📅 2026/8/5 2:20:23
JavaScript字符串截取与分割:slice、substring、split核心方法与Unicode处理
1. 从“小明和字符串”说起为什么字符串操作是基本功最近在社区里看到一个挺有意思的讨论有人分享了一道算法题大意是“小明和字符串”需要处理一些复杂的字符串匹配和截取逻辑。底下不少朋友在实现时代码写得冗长且容易出错。这让我想起刚入行那会儿处理用户输入的“21543655”和“4332656442”这类大数字字符串时也常常在substring、slice和substr之间犯迷糊更别提用split做复杂分割时的一头雾水。字符串处理可以说是JavaScript乃至任何编程语言中最基础、最高频的操作没有之一。从简单的用户输入校验、URL参数解析到复杂的数据清洗、模板渲染都离不开它。但正因为太基础很多人反而对其各种方法的细微差别不求甚解导致代码里埋下隐患。比如你是否清楚str.slice(-3)和str.substring(str.length-3)在语义和性能上的区别又是否知道在处理包含多字节字符如Emoji或中文的字符串时某些方法可能会让你掉进坑里这篇文章我们就来彻底厘清JavaScript中切割截取字符串的几种核心方法。我不会仅仅罗列API文档而是会结合真实的开发场景、性能考量以及那些容易踩坑的细节帮你构建起一套坚实且实用的字符串处理心智模型。无论你是正在苦于“JavaScript面试题”中字符串相关难题还是在实战中遇到了“将字符串转换为uniqueidentifier时失败”这类诡异错误相信都能在这里找到答案。2. 核心三剑客slice、substring、substr的深度辨析当我们需要从字符串中提取一部分时首先想到的就是slice、substring和substr。它们功能相似但细节上的差异决定了不同的使用场景。很多人死记硬背参数规则其实理解了设计初衷就再也无需记忆。2.1String.prototype.slice(beginIndex[, endIndex])最灵活的首选slice的设计理念来源于数组的同名方法行为也高度一致即“切片”。它的参数代表了切片的起始和结束位置索引。基本逻辑beginIndex从该索引处开始提取包含该字符。如果为负数则被视为str.length beginIndex即从末尾开始倒数。endIndex在该索引处结束提取不包含该字符。如果省略则一直提取到字符串末尾。如果为负数同样被视为str.length endIndex。const str Hello, world!; console.log(str.slice(0, 5)); // Hello - 从索引0到5不包含5 console.log(str.slice(7)); // world! - 从索引7到末尾 console.log(str.slice(-6)); // world! - 从倒数第6个字符到末尾 console.log(str.slice(7, -1)); // world - 从索引7到倒数第1个不包含 console.log(str.slice(-6, -1)); // world - 从倒数第6个到倒数第1个为什么slice是我的首选一致性与Array.prototype.slice的行为完全一致学会一个两个都会。这种跨数据类型的一致性减少了心智负担。负数索引支持这是它最大的优势。在处理“获取最后N个字符”或“去掉首尾字符”这类场景时使用负数索引让代码意图非常清晰无需先计算字符串长度。例如str.slice(-3)一眼就能看出是取最后三个字符。参数灵活endIndex可以小于beginIndex吗在slice中如果beginIndex大于等于endIndex则返回空字符串。这虽然不常用但定义明确不会出错。注意slice不会修改原字符串而是返回一个新的字符串。这是所有字符串方法的共同特点因为字符串在JavaScript中是不可变的Immutable。2.2String.prototype.substring(indexStart[, indexEnd])历史包袱与明确边界substring是更早出现的方法。它的核心特点是自动交换参数且负数参数被视为0。基本逻辑indexStart起始索引。indexEnd结束索引不包含。关键行为如果indexStart大于indexEnd则substring会自动交换这两个参数。如果任一参数小于0或为NaN则被视为0。const str Hello, world!; console.log(str.substring(0, 5)); // Hello console.log(str.substring(7)); // world! console.log(str.substring(7, 12)); // world // 参数自动交换 console.log(str.substring(5, 0)); // Hello - 等同于 substring(0,5) // 负数处理 console.log(str.substring(-3)); // Hello, world! - -3被视作0 console.log(str.substring(7, -2)); // Hello, - -2被视作0然后交换参数等同于 substring(0,7)substring的适用场景与坑明确的正数索引当你非常确定索引是正数且希望代码对参数顺序不敏感时虽然我不建议依赖这个特性substring可能有一定可读性。但事实上依赖自动交换是一种不好的实践它掩盖了程序逻辑。最大的坑负数参数。str.substring(-3)并不会如你所愿地返回最后三个字符而是返回整个字符串因为-3被转换成了0。这是很多Bug的来源。如果你从其他语言如Python转过来或者习惯了slice的负数索引这里极易踩坑。历史原因在现代JavaScript开发中除非维护遗留代码否则没有理由优先选择substringoverslice。slice的功能是substring的超集且行为更可预测。2.3String.prototype.substr(start[, length])已被废弃的“异类”substr的参数含义与前两者不同它用start和length长度来定义子串。基本逻辑start起始位置。如果是负数则从字符串末尾开始计算。length要提取的字符数。如果省略则提取到字符串末尾。const str Hello, world!; console.log(str.substr(0, 5)); // Hello console.log(str.substr(7)); // world! console.log(str.substr(-6)); // world! - 从倒数第6个字符开始到末尾 console.log(str.substr(7, 5)); // world为什么应该避免使用substr非标准与废弃substr虽然被广泛实现但它并非ECMAScript核心标准的一部分。在ECMA-262标准中它被标记为“遗留的”legacy和“非核心的”non-core。这意味着未来的标准可能会移除它尽管浏览器为了兼容性短期内不会。在严格模式strict mode下它的行为虽然不变但作为开发者我们应该避免使用被标记为遗留的特性。参数语义混淆slice(begin, end)和substring(start, end)都使用(开始 结束)的索引对而substr(start, length)使用了(开始 长度)。这种不一致性增加了学习和记忆成本在团队协作中容易引起误解。功能可替代substr的所有功能都可以用slice清晰表达str.substr(start, length)等价于str.slice(start, start length)当start非负时。str.substr(-start)等价于str.slice(-start)。实操建议在新项目中请完全放弃使用substr。如果遇到旧代码也应有计划地将其重构为slice。2.4 对比总结与选型指南为了更直观我们用一个表格来总结特性slice(start, end)substring(start, end)substr(start, length)参数含义开始索引 结束索引开始索引 结束索引开始索引 字符长度负数start从末尾倒数视为0从末尾倒数负数end/length从末尾倒数视为0length为负则视为0start end返回空字符串自动交换参数(不适用因第二参数是长度)标准化ECMAScript核心标准ECMAScript核心标准遗留特性非核心标准推荐度★★★★★ (首选)★★☆ (特定场景/旧代码)★☆☆ (避免使用)选型心法无脑用slice在绝大多数需要根据位置截取子串的场景下slice都是最佳选择。它标准、灵活支持负数、意图清晰。何时考虑substring几乎不需要。唯一能想到的场景是你故意希望负数参数被当作0处理但这种需求极少且可以通过Math.max(start, 0)更明确地实现。彻底告别substr从你的知识库和代码库中划掉它。3. 分割大师String.prototype.split(separator, limit)的进阶玩法如果说slice系列是“剪裁”那么split就是“拆分”。它将一个字符串分割成一个字符串数组是处理结构化文本如CSV、日志、路径的利器。但它的能力远不止用逗号分隔那么简单。3.1 基础分割字符串分隔符最常用的方式是用一个字符串作为分隔符。const csv apple,banana,orange; const fruits csv.split(,); console.log(fruits); // [apple, banana, orange] const path /home/user/documents/file.txt; const parts path.split(/); console.log(parts); // [, home, user, documents, file.txt] // 注意第一个空字符串因为字符串以分隔符开头重要细节如果分隔符是空字符串则会将字符串拆分为单个字符的数组。const str Hello; console.log(str.split()); // [H, e, l, l, o]如果分隔符在字符串中不存在则返回一个只包含原字符串的数组。const str Hello; console.log(str.split(x)); // [Hello]3.2 高级分割正则表达式分隔符这是split威力倍增的地方。你可以使用正则表达式来定义复杂的分割规则。const text apple, banana; orange. pear; // 使用正则匹配逗号、分号、点号及紧随的空格 const fruits text.split(/[,;.]\s*/); console.log(fruits); // [apple, banana, orange, pear] // 一个实用的例子分割驼峰命名 const camelCase getElementById; const words camelCase.split(/(?[A-Z])/); // 使用零宽正向先行断言在大写字母前分割 console.log(words); // [get, Element, By, Id]使用正则时的坑如果正则表达式包含捕获组则捕获组匹配的内容也会包含在返回的数组中。const str 2023-01-01; const parts str.split(/(-)/); // 分隔符“-”被捕获 console.log(parts); // [2023, -, 01, -, 01]这通常不是你想要的。为了避免这种情况可以使用非捕获组(?:...)。const parts2 str.split(/(?:-)/); console.log(parts2); // [2023, 01, 01]3.3 限制分割次数limit参数split的第二个可选参数limit是一个整数用于限制返回数组的最大长度。达到限制后剩余部分将不再分割会作为最后一个元素完整保留。const str a,b,c,d,e; console.log(str.split(,, 3)); // [a, b, c] - 只取前三个结果 console.log(str.split(,, 0)); // [] - limit为0返回空数组这个特性在解析类似“键值”对但值中可能包含分隔符时非常有用。不过更复杂的解析通常需要更强大的工具如正则表达式的exec循环。3.4 实战避坑与slice组合实现高效字符串处理split常与数组方法mapfilterjoin以及字符串方法组合形成强大的数据处理流水线。场景你有一串由不同分隔符连接的标签字符串需要清洗并获取前三个标签。const tagString javascript, algorithm; python, c ; // 目标得到 [javascript, algorithm, python] const tags tagString .split(/[,;]\s*/) // 按逗号或分号分割并去除紧跟的空格 .map(tag tag.trim()) // 去除每个标签首尾可能残留的空格 .filter(tag tag.length 0) // 过滤掉空字符串 .slice(0, 3); // 取前三个 console.log(tags); // [javascript, algorithm, python]一个常见的性能陷阱对于非常大的字符串如果只需要前面一部分先split再slice可能会创建不必要的中间数组消耗内存。如果分隔符简单有时用indexOf循环配合slice手动解析会更高效。但对于大多数日常场景可读性优先无需过早优化。4. 处理Unicode与多字节字符的“天坑”这是字符串处理中高级但至关重要的一环。JavaScript在ES6之前字符串长度和索引都是基于UTF-16编码的“代码单元”而不是我们直观理解的“字符”。这会导致处理Emoji、某些中文古字如“”等需要两个UTF-16代码单元即一个代理对表示的字符时出现问题。4.1 问题重现当length和slice不再可靠const emoji ; const complexChar ; // 这是一个需要代理对的汉字 console.log(emoji.length); // 2 不是一个“字符” console.log(complexChar.length); // 2 console.log(emoji.charAt(0)); // (乱码) console.log(emoji.charAt(1)); // (乱码) console.log(emoji.slice(0, 1)); // (乱码)你看我们以为的一个“笑脸”字符在JavaScript内部是用两个代码单元表示的。用传统的length属性和基于索引的方法去处理就会得到错误的结果。4.2 解决方案使用ES6的迭代与Array.fromES6为字符串引入了迭代器协议使其可以按Unicode码点可以粗略理解为“字符”进行迭代。方法一使用扩展运算符...或Array.fromconst emoji Hello; // 扩展运算符 const chars [...emoji]; console.log(chars); // [, H, e, l, l, o, ] console.log(chars.length); // 7 (正确) // Array.from 效果相同 const chars2 Array.from(emoji); console.log(chars2.length); // 7现在你可以安全地对这个字符数组进行操作比如截取前N个“真字符”。方法二使用for...of循环for...of循环字符串时也是按码点迭代的。let count 0; for (const char of Hello) { count; } console.log(count); // 74.3 实战安全地截取包含Emoji的字符串假设我们需要一个函数安全地截取字符串的前N个“可视字符”并避免切断Emoji。function safeSubstring(str, n) { // 将字符串转换为按码点分割的数组 const points Array.from(str); // 如果所需长度超过数组长度则返回整个数组拼接的字符串 if (n points.length) { return str; } // 取前N个码点再拼接回字符串 return points.slice(0, n).join(); } const text 你好世界; console.log(safeSubstring(text, 3)); // 你好 console.log(safeSubstring(text, 4)); // 你好世 console.log(safeSubstring(text, 5)); // 你好世界 console.log(text.slice(0, 5)); // 你好 (乱码)性能考量Array.from(str)会创建一个新数组对于超长字符串如一篇长文如果频繁操作可能会有性能开销。但在绝大多数UI展示、用户名/简介截断等场景下这个开销是可接受的。如果是在高性能密集的文本处理后端可能需要更底层的解决方案比如使用专门的字符串处理库。注意即使使用上述方法对于某些复杂的组合字符序列如一个基础字符加上多个组合变音符号可能仍然无法完美处理为一个“视觉字符”。对于需要极端精确的国际化和本地化应用建议使用如Intl.Segmenter目前处于Stage 4提案部分浏览器已支持等专门API。5. 性能对决与内存浅析在大多数前端应用中字符串操作的性能差异微乎其微不值得过度优化。但对于某些高频、处理大量数据的场景如在线编辑器、实时日志处理了解底层机制有助于做出更优选择。5.1slicevssubstringvssubstr的性能现代JavaScript引擎V8 SpiderMonkey JavaScriptCore对所有这些方法都进行了高度优化。通常slice和substring的性能在同一量级substr可能因非标准而略慢但差异极小。更重要的性能考量来自于使用方式避免在循环中重复计算长度for (let i 0; i str.length; i)中的str.length会被缓存引擎会优化。但如果你在循环体内频繁调用str.slice(i, i1)不如直接使用str[i]字符索引或str.charAt(i)。链式调用str.trim().slice(1, -1).toUpperCase()会创建多个中间字符串。对于超长字符串这可能带来内存和性能压力。如果可能尝试合并操作或使用正则表达式一次性完成。5.2 字符串的不可变性与内存JavaScript中的字符串是不可变的。这意味着任何“修改”字符串的操作slicesubstringtoUpperCasereplace等都会在内存中创建一个全新的字符串。let str Hello; let newStr str.slice(0, 3); // “Hel” // 内存中现在有两个字符串“Hello”和“Hel”。 // str 仍然指向 “Hello”newStr 指向新的字符串 “Hel”。这意味着什么优点线程安全简化了字符串的共享和比较。缺点频繁的字符串拼接或修改操作尤其是在循环中会产生大量临时字符串导致垃圾回收压力增大性能下降。经典的性能反例循环拼接字符串// 低效做法 let result ; for (let i 0; i 10000; i) { result data i; // 每次迭代都创建新字符串 }高效做法使用数组收集最后joinconst parts []; for (let i 0; i 10000; i) { parts.push(data i); } let result parts.join(); // 只创建一次最终字符串对于现代JS引擎简单的拼接在少数几次操作中可能被优化。但作为最佳实践当拼接次数很多或字符串很大时使用数组join是更可靠的选择。ES6的模板字符串在大多数场景下性能也很好且可读性更佳。6. 综合实战解析一个复杂字符串案例让我们运用所学解决一个融合了多种操作的实战问题。假设我们有一个从某系统导出的、格式不太规范的日志字符串需要提取关键信息。原始字符串“2023-10-27 14:35:22 [ERROR] moduleuserService, id‘11523’, msg‘Failed to connect to database: Network timeout’“目标提取出id的值11523和msg中冒号后的具体错误原因Network timeout。const log 2023-10-27 14:35:22 [ERROR] moduleuserService, id11523, msgFailed to connect to database: Network timeout; // 步骤1按空格分割获取日志级别后面的部分 const partsAfterLevel log.split(] )[1]; // “moduleuserService, id11523, msgFailed to connect to database: Network timeout” console.log(步骤1结果:, partsAfterLevel); // 步骤2按逗号加空格分割得到各个键值对 const keyValuePairs partsAfterLevel.split(, ); console.log(步骤2结果:, keyValuePairs); // [“moduleuserService”, “id11523”, “msgFailed to connect to database: Network timeout”] // 步骤3遍历键值对找到id和msg let extractedId null; let extractedError null; for (const pair of keyValuePairs) { // 使用split(‘’)分割键和值 const [key, value] pair.split(); // 注意value还带着引号 if (key id) { // 去除值两端的单引号 extractedId value.slice(1, -1); // 从第1个索引到倒数第1个索引不包含 } else if (key msg) { // 去除值两端的单引号 const fullMsg value.slice(1, -1); // 提取冒号后的部分并去除可能的前后空格 extractedError fullMsg.split(: )[1]; } } console.log(提取的ID:, extractedId); // “11523” console.log(提取的错误原因:, extractedError); // “Network timeout”代码分析log.split(] )利用日志格式中“]”后总跟一个空格的特征快速定位到消息主体部分。partsAfterLevel.split(, ): 假设键值对之间用“逗号空格”分隔。这是一个脆弱的假设如果msg内部包含逗号就会出错。更健壮的做法是用正则表达式匹配/\w([^]*|[^,])/g但为了示例清晰这里用了简单方法。value.slice(1, -1)这是去除首尾单引号的优雅写法。slice(1)去掉第一个字符slice(-1)不传参数表示到末尾但我们传了-1表示到倒数第一个不包含所以正好去掉了首尾字符。fullMsg.split(: )假设错误消息的格式固定用“冒号空格”分割描述和具体原因。这个案例的启示字符串处理很少只用一种方法通常是split、slice、indexOf、正则等组合拳。对数据格式的假设至关重要。不严谨的分隔符选择会导致解析失败。在实际开发中需要仔细审查数据样本或与数据提供方约定规范。像slice(1, -1)这样的技巧用好了能让代码非常简洁。但也要注意如果值可能没有引号或者引号类型不固定单引号/双引号就需要更复杂的逻辑来处理。7. 扩展思考indexOf、substring与手动解析有时split显得过于“重”或者分隔符模式不固定这时结合indexOf或lastIndexOf和slice/substring进行手动解析会更灵活。场景从一个URL的查询字符串中提取特定参数值。function getQueryParam(url, paramName) { // 1. 找到查询字符串起始点 const queryIndex url.indexOf(?); if (queryIndex -1) return null; const queryString url.slice(queryIndex 1); // 2. 分割每个参数 const params queryString.split(); for (const param of params) { // 3. 找到第一个‘’的位置 const equalSignIndex param.indexOf(); if (equalSignIndex -1) continue; // 跳过没有值的参数 const key param.slice(0, equalSignIndex); const value param.slice(equalSignIndex 1); if (key paramName) { return decodeURIComponent(value); // 解码URL编码的值 } } return null; } const url https://example.com/page?name小明id1125filteractive; console.log(getQueryParam(url, name)); // “小明” console.log(getQueryParam(url, id)); // “1125” console.log(getQueryParam(url, token)); // null为什么不用split(‘’)一次性分割参数因为参数值本身可能包含等号尽管不常见。使用indexOf找到第一个等号的位置可以确保正确分割键和值。这种“手动解析”提供了更强的控制力。另一个常用技巧lastIndexOf当你需要从路径中获取文件名时const path /home/user/docs/archive.zip; const filename path.slice(path.lastIndexOf(/) 1); console.log(filename); // “archive.zip”lastIndexOf找到最后一个分隔符的位置然后slice截取其后部分比用split(‘/’).pop()在内存上更高效因为它避免了创建中间数组。字符串处理的世界远不止这几种方法还有padStart/padEnd、trim系列、startsWith/endsWith、includes、repeat等非常实用的方法。但slice、substring、split无疑是构建复杂操作的基石。理解它们的本质差异、性能特性和适用边界能让你在遇到“小明和字符串”这类问题时写出更稳健、更高效的代码。记住在JavaScript中slice是你的瑞士军刀而警惕Unicode“字符”与“代码单元”的差异则是通往高级开发的必经之路。