js substring 遇到负数和参数顺序颠倒时会怎样?解读它的处理规则

AI资讯1小时前发布 jdkjadmin
0 00
js substring 遇到负数和参数顺序颠倒时会怎样?解读它的处理规则

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() 时,重点记住以下几点:

  1. 起始位置包含,结束位置不包含。
  2. 负数会按 0 处理,不表示从末尾倒数。
  3. 起始位置大于结束位置时,两个位置会自动交换。
  4. 越界位置会限制到字符串长度范围内。
  5. 小数会向零方向取整;NaN 按 0 处理;特殊类型可能因转换而抛出异常。
  6. 索引和 length 按 UTF-16 代码单元计算,不一定等于用户可见字符数。
  7. 它只负责截取字符串,不负责验证、过滤或保证输入安全。

如果需求包含负数索引,优先考虑 slice();如果参数顺序有业务意义,就在调用前显式校验;如果要按用户感知的字符截取,则先确认正确的 Unicode 切分单位。把这些边界分清楚,substring() 的行为就不难预测。

© 版权声明

相关文章