
js substring 和 slice 有什么区别?用例子讲清楚。
JavaScript 中,substring() 和 slice() 都能从字符串中取出一段内容。它们的基本调用形式相似,结果也都不包含结束位置对应的字符,因此刚开始使用时很容易混淆。
const text = "JavaScript";
console.log(text.substring(0, 4)); // "Java"
console.log(text.slice(0, 4)); // "Java"
真正的区别主要在参数边界:substring() 会把负数当作 0,并在起始位置大于结束位置时交换两个位置;slice() 支持负数从字符串末尾倒数,且不会交换参数。理解这两点,日常使用通常就不会出错。
先看共同点:结束位置不包含在结果中
这两个方法都可以传入起始位置和结束位置:
str.substring(start, end)
str.slice(start, end)
起始位置包含在结果中,结束位置不包含在结果中。可以把它理解为取一个左闭右开的范围:从 start 开始,取到 end 之前。
const str = "abcdef";
console.log(str.slice(1, 4)); // "bcd"
console.log(str.substring(1, 4)); // "bcd"
位置按从 0 开始计算:a 在位置 0,b 在位置 1,以此类推。上例取出位置 1、2、3 的字符,不包含位置 4 的 e。
这种“结束位置不包含在结果中”的规则,也让长度计算变得简单:如果起始位置是 2,结束位置是 5,最多取出 5 - 2 个 UTF-16 代码单元。
核心区别一:负数参数
slice():负数从字符串末尾倒数
slice() 的负数位置表示从末尾倒数。字符串长度为 length 时,负数位置大致相当于 length + 位置。换算后仍小于 0 的位置会按 0 处理;大于字符串长度的位置会按字符串长度处理。
const str = "abcdef";
console.log(str.slice(-2)); // "ef"
console.log(str.slice(-4, -1)); // "cde"
console.log(str.slice(-20, 2)); // "ab"
slice(-2) 从末尾向前数两个位置,也就是从 e 开始取到字符串末尾。slice(-4, -1) 相当于 slice(2, 5),结果是 cde。
substring():负数按 0 处理
substring() 不把负数解释为“从末尾倒数”。负数会按 0 处理,所以它不适合用负数表达尾部位置。
const str = "abcdef";
console.log(str.substring(-2)); // "abcdef"
console.log(str.substring(-4, 2)); // "ab"
console.log(str.substring(-4, -1)); // ""
第一行的起始位置 -2 被当成 0,省略的结束位置是字符串长度,因此结果是整个字符串。第三行中两个负数都被当成 0,结果相当于 substring(0, 0),所以是空字符串。
如果需求是“取最后几个字符”,通常用 slice() 更直接:
const filename = "report.csv";
console.log(filename.slice(-3)); // "csv"
核心区别二:起始位置大于结束位置
substring() 会在起始位置大于结束位置时交换两者。slice() 不会交换;如果起始位置不小于结束位置,结果就是空字符串。
const str = "abcdef";
console.log(str.substring(4, 1)); // "bcd"
console.log(str.slice(4, 1)); // ""
可以把第一行理解为 substring(1, 4),所以得到 bcd。而 slice(4, 1) 是从位置 4 取到位置 1 之前,范围方向不成立,因此返回空字符串。
当两个参数相等时,两种方法都会返回空字符串:
const str = "abcdef";
console.log(str.slice(3, 3)); // ""
console.log(str.substring(3, 3)); // ""
如果代码里可能出现参数顺序颠倒,要特别注意:substring() 会默默调整顺序,这有时方便,有时也会掩盖调用方的错误。slice() 不调整,更容易让错误的边界表现为空结果。
参数边界与特殊值
省略结束位置
两种方法都允许省略第二个参数。省略时,结束位置是字符串长度,也就是一直取到字符串末尾。
const str = "JavaScript";
console.log(str.slice(4)); // "Script"
console.log(str.substring(4)); // "Script"
越界位置会被限制在有效范围内
位置小于 0 或大于字符串长度时,两个方法都会按各自的规则把位置限制到有效范围内。区别是 slice() 会先按负数规则从末尾换算,substring() 则将负数置为 0。
const str = "abc";
console.log(str.slice(0, 20)); // "abc"
console.log(str.substring(0, 20)); // "abc"
console.log(str.slice(20, 30)); // ""
console.log(str.substring(20, 30)); // ""
如果起始位置超出字符串长度,结束位置也会限制到长度。起始位置此时不小于结束位置,两种方法都返回空字符串。
参数会转换为整数位置
参数不是字符串索引类型的专用值;JavaScript 会把它们转换为数字位置并取整。小数部分会向 0 的方向截断,NaN 按 0 处理。实践中最好传入明确的整数,避免依赖隐式转换。
const str = "abcdef";
console.log(str.slice(1.9, 4.8)); // "bcd",位置按 1 和 4 处理
console.log(str.slice(NaN, 2)); // "ab",NaN 按 0 处理
还要区分“省略参数”和“传入 undefined”:对于这两个方法,第二个参数为 undefined 时也会表现为使用字符串长度;但 null 会转换为数字 0。
const str = "abcdef";
console.log(str.slice(2, undefined)); // "cdef"
console.log(str.slice(2, null)); // ""
最后一行相当于从位置 2 取到位置 0,起始位置不小于结束位置,所以结果为空。不要把 null 和 undefined 当成相同的边界值。
对照表:什么时候该用哪个
| 行为 | slice(start, end) | substring(start, end) |
|---|---|---|
| 结束位置是否包含 | 不包含 | 不包含 |
| 负数位置 | 从末尾倒数 | 按 0 处理 |
| 起始位置大于结束位置 | 返回空字符串 | 交换两个位置 |
| 省略结束位置 | 取到字符串末尾 | 取到字符串末尾 |
简单选择建议:
- 需要从字符串末尾取内容,例如去掉扩展名、取末尾几位时,优先考虑
slice()。 - 需要按两个边界位置截取,且确定起始位置不大于结束位置时,两者都能用。
- 不希望参数顺序颠倒时仍得到一段非空内容,
slice()的行为通常更容易暴露问题。 - 如果现有项目或 API 明确要求
substring(),按约定使用即可,但不要把它的负数规则误认为与slice()相同。
常见用例
从指定位置取到末尾
例如从 URL 路径中取出某个前缀之后的部分。注意,截取本身并不会解析 URL;如果需要可靠地处理 URL,应使用 URL 等专用接口。
const path = "/docs/guide";
console.log(path.slice(6)); // "guide"
取末尾若干字符
const orderId = "ORD-2025-0086";
console.log(orderId.slice(-4)); // "0086"
这种用法直接表达了“从尾部取四个位置”。用 substring() 写同样需求,通常要先计算 length,可读性不如负数形式。
按固定边界取出一段
const date = "2025-04-18";
const year = date.slice(0, 4);
const month = date.slice(5, 7);
const day = date.slice(8, 10);
console.log(year, month, day); // "2025 04 18"
这个例子适用于格式已经明确、位置固定的字符串。对于用户输入或格式可能变化的数据,不要只靠固定位置截取来验证格式;应先解析或校验完整结构。
去掉已知前缀或后缀
const filename = "report.csv";
const extension = ".csv";
const name = filename.endsWith(extension)
? filename.slice(0, -extension.length)
: filename;
console.log(name); // "report"
slice(0, -extension.length) 利用负数结束位置,从末尾排除扩展名长度。但示例先用 endsWith() 检查后缀,避免文件名并没有该后缀时仍然错误截取。
这里的代码只是字符串处理示例,不代表检查扩展名就能验证文件类型,也不能替代服务端的文件安全校验。
常见错误及修正方式
把结束位置当成“要取的字符数”
例如想取前 3 个字符,却写成 slice(0, 3) 是正确的,因为结束位置是 3;但如果从位置 2 开始取 3 个字符,结束位置应为 5,而不是 3。
const str = "abcdef";
console.log(str.slice(2, 5)); // "cde",从位置 2 取 3 个位置
console.log(str.slice(2, 3)); // "c",不是 3 个字符
可用公式检查:当位置范围有效时,要取 count 个位置,结束位置通常是 start + count。
以为两种方法对负数处理相同
const str = "abcdef";
console.log(str.slice(-3)); // "def"
console.log(str.substring(-3)); // "abcdef"
如果代码里有负数边界,先确认需要的是“从末尾倒数”,还是“把负数当作 0”。不要因为两者在普通正数参数下结果相同,就认为它们可以随意替换。
以为 substring() 会保持参数方向
const start = 5;
const end = 2;
console.log("abcdef".substring(start, end)); // "cde"
console.log("abcdef".slice(start, end)); // ""
如果两个位置来自计算结果,建议在调用前明确校验它们。否则 substring() 的自动交换可能让结果看起来合理,却与原本的业务意图不同。
以为截取可以完整处理所有“字符”
JavaScript 字符串的 length、substring() 和 slice() 按 UTF-16 代码单元处理位置,不是按用户感知的字符(字素簇)处理。对于基本拉丁字母,这一区别通常不明显;对于 emoji、组合字符和某些文字字符,可能出现“一个视觉字符占多个位置”的情况。
const text = "A😀B";
console.log(text.length); // 4
console.log(text.slice(1, 2)); // 单独取出代理对的一半,显示效果可能异常
console.log(Array.from(text).length); // 3
这里的 😀 在 UTF-16 中由一对代理项表示,因此字符串长度是 4:A 占一个代码单元,emoji 占两个,B 占一个。按位置截取可能正好切开这对代理项,生成不完整的 Unicode 字符。
如果需求是按 Unicode 码点遍历,可以使用 Array.from(),但它仍不等同于按用户看到的字素簇截取。例如某些带组合音标的字符或由多个码点组成的 emoji,视觉上可能是一个字符,代码点层面却不止一个。
const text = "A😀B";
const codePoints = Array.from(text);
console.log(codePoints.slice(1, 2).join("")); // "😀"
如果需要按用户感知的字素簇截取,可在支持环境中使用 Intl.Segmenter:
const text = "A😀B";
if (typeof Intl.Segmenter === "function") {
const segmenter = new Intl.Segmenter("zh", {
granularity: "grapheme"
});
const graphemes = Array.from(segmenter.segment(text), item => item.segment);
console.log(graphemes.slice(1, 2).join("")); // "😀"
}
是否需要按码点或字素簇处理,取决于业务定义。两种方法本身不会自动按“屏幕上看见的字符”计数。
字符串截取不是安全过滤
slice() 和 substring() 只负责返回字符串的一部分,不会转义、验证或清理内容。截短用户输入不能代替 XSS 防护,也不能保证输入符合业务格式。
const userInput = "<script>alert(1)</script>";
const shortened = userInput.slice(0, 20);
// shortened 仍然是未经转义的文本。
// 不要把它直接拼进 HTML 字符串并作为可信标记输出。
把字符串写入网页时,应按输出位置使用合适的安全方式,例如在浏览器中将文本赋给元素的 textContent,而不是把不可信内容当 HTML 插入。若业务确实需要允许一部分 HTML,应使用经过维护的 HTML 清理方案,而不是靠截取字符串实现。
同样,截取字符串也不能代替权限检查、文件类型验证、SQL 参数化或服务端输入校验。它只改变字符串长度和内容范围,不改变其可信程度。
可运行的综合示例
下面的示例可以直接在浏览器控制台或 Node.js 中运行,对比两种方法在常见边界下的结果。
const text = "abcdef";
console.log("普通范围");
console.log(text.slice(1, 4)); // "bcd"
console.log(text.substring(1, 4)); // "bcd"
console.log("负数");
console.log(text.slice(-2)); // "ef"
console.log(text.substring(-2)); // "abcdef"
console.log("参数倒置");
console.log(text.slice(4, 1)); // ""
console.log(text.substring(4, 1)); // "bcd"
console.log("越界");
console.log(text.slice(0, 99)); // "abcdef"
console.log(text.substring(0, 99)); // "abcdef"
console.log("空范围");
console.log(text.slice(3, 3)); // ""
console.log(text.substring(3, 3)); // "");
总结
substring() 与 slice() 的共同点是都按起始位置(包含)和结束位置(不包含)截取字符串。最值得记住的区别只有两条:
slice()的负数位置从字符串末尾倒数;substring()的负数按 0 处理。slice()遇到起始位置大于结束位置时返回空字符串;substring()会交换两个位置。
按末尾取值、去掉已知后缀等场景,slice() 通常写起来更直接;普通的正数范围截取,两者都可以。无论选哪一个,都要记住结束位置不包含在结果中、位置按 UTF-16 代码单元计算,并且截取不是输入验证或安全过滤。




