
js substring 遇到负数和参数顺序颠倒时会怎样?解读它的处理规则
JavaScript 的 substring() 用来从字符串中截取一段内容。它的两个参数看起来很简单:起始位置和结束位置。但遇到负数、超过字符串长度的索引,或者把起止位置写反时,它的行为并不总是直觉所想。
先记住最重要的规则:substring(start, end) 会把两个索引限制在字符串范围内;负数按 0 处理;如果起始索引大于结束索引,方法会自动交换两者;结束位置不包含在结果里。
const text = "JavaScript";
console.log(text.substring(0, 4)); // "Java"
console.log(text.substring(4, 0)); // "Java"
console.log(text.substring(-3, 4)); // "Java"
console.log(text.substring(4, 100)); // "Script"
理解这些规则,能避免截取结果与预期不符,也有助于区分 substring()、slice() 和旧式的 substr()。本文从基本语义讲起,再讨论参数转换、Unicode、常见错误和安全边界。
一、substring 的基本用法
substring() 是字符串实例方法,常用形式有两种:
str.substring(indexStart)
str.substring(indexStart, indexEnd)
indexStart:截取的起始位置,包含该位置的字符。indexEnd:截取的结束位置,不包含该位置的字符。
如果只提供一个参数,截取范围从该位置一直延伸到字符串末尾,相当于把字符串长度作为结束索引:
const text = "JavaScript";
console.log(text.substring(4)); // "Script"
console.log(text.substring(10)); // ""
console.log(text.substring(11)); // ""
结束索引不包含在结果中,这一点与数组的 slice() 等常见区间操作一致。可以把截取区间理解为左闭右开区间 [start, end)。例如 substring(2, 5) 取索引 2、3、4,不取索引 5。
二、负数会被当作 0
substring() 不使用负数表示“从字符串末尾倒数”。负数会被转换为整数后,再限制到 0。因此,负数起始位置通常等价于从字符串开头开始截取。
const text = "abcdef";
console.log(text.substring(-2, 3)); // "abc"
console.log(text.substring(-2)); // "abcdef"
console.log(text.substring(-5, -1)); // ""
逐项看最后一个例子:-5 和 -1 都会变成 0,实际调用效果相当于 text.substring(0, 0),所以结果是空字符串。
如果负数参数与正数参数同时出现,负数先变成 0,再继续按正常规则截取:
const text = "abcdef";
console.log(text.substring(-2, 4)); // "abcd"
console.log(text.substring(4, -2)); // "abcd"
第二行看起来更容易误解。它先把结束参数 -2 处理成 0,接着发现起始位置 4 大于结束位置 0,于是交换两个位置,最终取到索引 0 到 4 之前的内容。
因此,下面这种写法通常不能实现“从末尾倒数”的效果:
const text = "abcdef";
// 不是“从倒数第 3 位截到末尾”
console.log(text.substring(-3)); // "abcdef"
如果要用负数表示从末尾倒数的位置,通常应考虑 slice():
const text = "abcdef";
console.log(text.slice(-3)); // "def"
console.log(text.slice(-4, -1)); // "cde"
三、参数顺序颠倒时会自动交换
与 slice() 不同,substring() 在起始位置大于结束位置时,不会返回空字符串,也不会从右往左截取,而是把两个位置交换。
const text = "JavaScript";
console.log(text.substring(0, 4)); // "Java"
console.log(text.substring(4, 0)); // "Java"
可以把下面两次调用看作等价:
text.substring(4, 0)
text.substring(0, 4)
这种自动交换有时显得方便,但也可能掩盖参数计算错误。比如程序本来期待“开始位置必须小于结束位置”,却无意中传反了参数,substring() 仍会返回一段看似合理的文本,错误不一定立刻暴露。
若起止位置来自变量,建议明确变量含义,并在需要时主动校验:
function getRange(text, start, end) {
if (start > end) {
throw new RangeError("start 不能大于 end");
}
return text.substring(start, end);
}
console.log(getRange("abcdef", 1, 4)); // "bcd"
// 取消下一行注释会抛出 RangeError
// console.log(getRange("abcdef", 4, 1));
如果业务本身需要无论参数顺序如何都取两者之间的内容,可以利用交换规则;如果顺序代表有意义的方向,最好自行校验,不要依赖自动交换。
四、超过字符串长度的索引会怎样
字符串的 length 表示其 UTF-16 代码单元数量。经过参数转换后,超出范围的索引会被限制在 0 到 length 之间。
- 小于
0的位置按0处理。 - 大于字符串长度的位置按
length处理。 - 等于字符串长度的位置指向字符串末尾。
const text = "abc";
console.log(text.substring(0, 100)); // "abc"
console.log(text.substring(100)); // ""
console.log(text.substring(2, 100)); // "c"
console.log(text.substring(3)); // ""
结束位置很大时,效果相当于截到末尾;起始位置很大时,起点和结束点都会落在末尾,因此结果为空字符串。
五、参数不是整数时会发生什么
substring() 会将参数转换为数值,再向零方向取整。也就是说,小数部分会被截掉,而不是四舍五入。
const text = "abcdef";
console.log(text.substring(1.9, 4.8)); // "bcd"
console.log(text.substring(-1.9, 3.9)); // "abc"
第一行的参数相当于 1 和 4;第二行则先将 -1.9 向零方向取整为 -1,再把负数限制为 0,结束位置变为 3。
其他常见转换结果包括:
const text = "abcdef";
console.log(text.substring(NaN, 3)); // "abc"
console.log(text.substring(1, Infinity)); // "bcdef"
console.log(text.substring(-Infinity, 2)); // "ab"
console.log(text.substring()); // "abcdef"
NaN 按 0 处理;正无穷大和负无穷大分别限制到字符串长度和 0。省略起始参数时,它会以 undefined 参与转换,得到 0;省略结束参数时,结束位置默认是字符串长度。因此,无参数调用会返回整个字符串的副本。
字符串形式的数字和布尔值也会先转换为数值:
const text = "abcdef";
console.log(text.substring("1", "4")); // "bcd"
console.log(text.substring(false, true)); // "a"
这类隐式转换虽然符合 JavaScript 规则,但在业务代码里不宜过度依赖。参数若来自表单、URL 或外部接口,最好先进行明确的类型和范围校验,避免意外值被悄悄转换成合法索引。
不能转换为普通数值的参数可能导致异常。例如传入 Symbol 或 BigInt 时,索引转换会抛出 TypeError:
const text = "abcdef";
// 分别运行会抛出 TypeError
// text.substring(Symbol("index"));
// text.substring(1n, 3);
如果参数是对象,JavaScript 还可能调用它的数值转换方法。对于不可信对象,不应假设参数转换没有副作用;应先将外部数据验证并转换为预期的数值类型。
六、substring 和 slice 的关键差别
两者都可以从字符串中截取内容,而且结束位置都不包含在结果里,但负数和反向参数的处理不同。
| 情况 | substring() | slice() |
|---|---|---|
| 负数索引 | 按 0 处理 | 从字符串末尾倒数 |
| 起点大于终点 | 交换起点和终点 | 返回空字符串 |
| 结束位置 | 不包含 | 不包含 |
const text = "abcdef";
console.log(text.substring(-3, 5)); // "abcde"
console.log(text.slice(-3, 5)); // "d"
console.log(text.substring(4, 1)); // "bcd"
console.log(text.slice(4, 1)); // ""
选择方法时,可以按需求判断:
- 需要负数索引表达“从末尾倒数”,通常选
slice()。 - 需要确保参数反向时仍截取两点之间的区间,可以使用
substring()。 - 希望反向参数直接得到空结果,
slice()的语义更明确。
substr(start, length) 是历史遗留方法,第二个参数表示长度,而不是结束位置,并且已不推荐用于新代码。例如 "abcdef".substr(1, 3) 得到 "bcd"。新代码宜使用 slice() 或 substring(),避免混淆参数含义。
七、length 和索引按 UTF-16 代码单元计算
substring() 的索引不是按用户眼中“字符”的数量计算,而是按 UTF-16 代码单元计算。大多数常见汉字和英文字母占一个代码单元,但部分表情符号等字符由一对代理项组成,占两个代码单元。
const text = "A😀B";
console.log(text.length); // 4
console.log(text.substring(0, 2)); // "A😀"
console.log(text.substring(1, 2)); // "�"
"A😀B" 有三个用户可见字符,但 length 是 4:字母 A 占一个代码单元,笑脸占两个,字母 B 占一个。第三行只取到笑脸的一个代理项,显示效果可能是替代字符,也可能因运行环境而异。这不是 substring() 随机出错,而是索引单位与用户可见字符不同。
若需要按 Unicode 码点迭代,可以使用扩展运算符或 Array.from():
const text = "A😀B";
const codePoints = Array.from(text);
console.log(codePoints.length); // 3
console.log(codePoints.slice(1, 2).join("")); // "😀"
不过,按码点处理仍不一定等同于按屏幕上的字素簇(用户感知的字符)处理。某些旗帜、肤色修饰表情和带组合附加符号的文字,可能由多个码点组合显示。需要按用户感知的字符切分时,可考虑 Intl.Segmenter,并确认目标运行环境支持:
const text = "A😀B";
if (typeof Intl.Segmenter === "function") {
const segmenter = new Intl.Segmenter(undefined, {
granularity: "grapheme"
});
const graphemes = Array.from(segmenter.segment(text), item => item.segment);
console.log(graphemes); // ["A", "😀", "B"]
}
因此,处理纯英文标识符或按 JavaScript 字符串索引操作时,substring() 通常足够;处理可能含表情符号或复杂文字的用户输入时,则应先明确索引究竟按代码单元、码点还是字素簇计算。
八、常见错误及修正方式
1. 以为负数代表从末尾倒数
const fileName = "report.csv";
console.log(fileName.substring(-3)); // "report.csv"
console.log(fileName.slice(-3)); // "csv"
需要末尾相对位置时,改用 slice(),或自行基于 length 计算索引。
2. 以为参数颠倒会得到空字符串
const text = "abcdef";
console.log(text.substring(5, 2)); // "cde"
console.log(text.slice(5, 2)); // ""
若参数顺序必须保持,调用前检查 start <= end;若使用 substring(),要记得它会交换参数。
3. 把第二个参数当作截取长度
const text = "abcdef";
console.log(text.substring(2, 4)); // "cd",到索引 4 结束
console.log(text.slice(2, 4)); // "cd",同样是结束索引
// 若想从索引 2 开始取 4 个代码单元:
console.log(text.slice(2, 2 + 4)); // "cdef"
substring() 的第二个参数是结束索引,不是长度。把它误当成长度,是从 substr() 切换到现代方法时常见的迁移错误。
4. 忘记结束位置不包含
如果需要截取索引 2、3、4,应写 substring(2, 5),而不是 substring(2, 4)。计算范围时可以先写清楚左闭右开区间,减少差一位问题。
5. 认为索引一定代表用户看到的字符
对于包含代理对的字符,length 和截取位置按 UTF-16 代码单元计算。不能假设索引 1 就是第二个用户可见字符,也不要在不确认文本内容时用任意索引切分多语言文本。
九、适用场景与边界
substring() 适合按已知索引范围读取字符串,例如从固定格式文本中提取一段内容、截取展示用的片段,或在索引经过可靠校验后取得子串。它不会修改原字符串,而是返回一个新字符串;字符串本身不可变。
const record = "2025-03-08";
const year = record.substring(0, 4);
const month = record.substring(5, 7);
console.log(year); // "2025"
console.log(month); // "03"
console.log(record); // "2025-03-08"
但固定位置解析只有在输入格式已经验证的前提下才可靠。若日期格式可能变化,或输入来自外部,应使用适合该格式的解析和校验逻辑,而不是仅凭几个固定索引认定内容有效。
substring() 也不是安全过滤器。它不会检查字符串是否包含 HTML、脚本、SQL 片段、路径穿越内容或其他危险字符;截短字符串不能替代输出编码、参数化查询、路径校验或业务规则检查。比如把用户输入截短到 100 个代码单元,并不能保证它适合直接插入 HTML。
对于外部传入的索引,可以先验证类型和范围,避免隐式转换及无意间交换参数:
function safeSubstring(text, start, end) {
if (typeof text !== "string") {
throw new TypeError("text 必须是字符串");
}
if (!Number.isInteger(start) || !Number.isInteger(end)) {
throw new TypeError("start 和 end 必须是整数");
}
if (start < 0 || end < start || end > text.length) {
throw new RangeError("索引范围无效");
}
return text.substring(start, end);
}
console.log(safeSubstring("abcdef", 1, 4)); // "bcd"
这个例子把索引限制在 UTF-16 代码单元范围内,也拒绝反向区间。若业务采用码点或字素簇作为索引单位,需要另行设计与该单位一致的切分逻辑,不能直接套用 string.length 与 substring()。
十、可以直接运行的规则检查
下面的代码可在浏览器控制台或 Node.js 中运行,用断言检查几条容易混淆的规则:
const text = "abcdef";
console.assert(text.substring(-2, 3) === "abc");
console.assert(text.substring(4, 1) === "bcd");
console.assert(text.substring(2, 100) === "cdef");
console.assert(text.substring(2, 2) === "");
console.assert(text.slice(-2) === "ef");
console.assert(text.slice(4, 1) === "");
console.log("检查完成");
总结
使用 substring() 时,重点记住以下几点:
- 起始位置包含,结束位置不包含。
- 负数会按
0处理,不表示从末尾倒数。 - 起始位置大于结束位置时,两个位置会自动交换。
- 越界位置会限制到字符串长度范围内。
- 小数会向零方向取整;
NaN按 0 处理;特殊类型可能因转换而抛出异常。 - 索引和
length按 UTF-16 代码单元计算,不一定等于用户可见字符数。 - 它只负责截取字符串,不负责验证、过滤或保证输入安全。
如果需求包含负数索引,优先考虑 slice();如果参数顺序有业务意义,就在调用前显式校验;如果要按用户感知的字符截取,则先确认正确的 Unicode 切分单位。把这些边界分清楚,substring() 的行为就不难预测。




