JavaScript % 取余运算符基础用法与返回值类型
深入讲解 JavaScript 中 % 取余运算符的基础用法、底层数学机制、向零截断与取模的区别,以及不同操作数类型下的返回值类型与边界处理。
% 是取余运算符(Remainder Operator),其核心功能是计算两个操作数相除后的代数余数。该运算最关键的特性是:计算结果的正负符号完全取决于左操作数(被除数),且底层的商数采用向零截断(Truncated Division)方式计算,这与数学及 Python 等语言中的“向下取整取模(Modulo)”有着本质区别。在返回值类型方面,若两个操作数均为 BigInt,则运算返回 bigint;在常规数值运算下返回 number(若除数为 0 或存在非法类型转换,则返回属于 number 类型的 NaN)。
一、% 取余运算符的基础语法与计算机制
在 JavaScript 中,取余运算符写作英文百分号 %,它属于双目二元算术运算符。基础语法形式为:
// 语法格式:被除数 % 除数
const remainder = dividend % divisor;
许多初学者直觉上认为 a % b 仅仅是“小学除法剩下的数”。但在 ECMAScript 规范中,其执行逻辑有严格的代数推导定义。设左操作数为 a,右操作数为 b,JavaScript 引擎执行的代数推导过程如下:
- 计算两数相除的商:
q = Math.trunc(a / b)。注意这里的截断函数Math.trunc()是直接抹去小数部分,使商向 0 靠拢。 - 根据商回推余数:
remainder = a - (b * q)。
为了让这一底层的推导过程更加直观,下图完整展示了引擎在接收表达式后的操作数类型转换、有效性检查、截断商计算到最终余数导出的执行管道:
通过上述推导公式,我们可以清晰地理解常规正整数运算的结果:
// 示例 1:常规正整数取余
const r1 = 10 % 3; // 10 - (3 * 3) = 1
const r2 = 12 % 4; // 12 - (4 * 3) = 0 (整除)
const r3 = 3 % 5; // 3 - (5 * 0) = 3 (被除数小于除数,直接返回被除数)
console.log(r1, r2, r3);
// 示例输出: 1 0 3
二、负数取余与“取余 vs 取模”的核心差异
在工程实践中,涉及负数时的表现往往是引发 Bug 的重灾区。很多开发者将 % 称为“取模运算符”,但事实上,取余(Remainder)与取模(Modulo)在除数为负或被除数为负时并不等价。
两者的根本分歧在于商数的舍入方式:
- JavaScript 取余(Truncated Division):商数向 0 截断(
Math.trunc)。余数的符号与被除数(左操作数)相同。 - 数学 / Python 取模(Floored Division):商数向负无穷向下取整(
Math.floor)。余数的符号与除数(右操作数)相同。
让我们以 -7 % 3 和 7 % -3 为例,观察代数运算的实际推导:
// 示例 2:正负符号组合验证
console.log(7 % 3); // 7 - (3 * 2) = 1
console.log(-7 % 3); // -7 - (3 * -2) = -1 (符号同左侧 -7)
console.log(7 % -3); // 7 - (-3 * -2) = 1 (符号同左侧 7)
console.log(-7 % -3); // -7 - (-3 * 2) = -1 (符号同左侧 -7)
// 示例输出: 1, -1, 1, -1
num % 2 === 1,在处理负数时会出现严重错误。例如 -3 % 2 的计算结果为 -1,表达式 -1 === 1 判定为 false,导致负奇数被误判为非奇数。正确写法:应该判断余数是否不为 0,即
num % 2 !== 0;或者先使用绝对值 Math.abs(num) % 2 === 1。
在 JavaScript 中实现真正的模运算(Modulo)
若业务场景需要严格的数学模运算(例如时钟回转、负索引循环数组等,要求结果始终落在 [0, divisor - 1] 区间内),可使用经典的偏移修正公式:
// 通用取模函数:保证结果与除数同号,且非负(当除数为正数时)
function modulo(n, m) {
return ((n % m) + m) % m;
}
console.log(modulo(-7, 3)); // ((-7 % 3) + 3) % 3 = (-1 + 3) % 3 = 2
// 示例输出: 2
三、返回值类型全面解析
在 JavaScript 中,取余运算符并非只返回普通数字,其返回值类型受操作数的原始类型及 ECMAScript 类型转换规则共同决定。主要分为以下三种情形:
1. number 类型返回值
这是最常见的返回类型。在绝大多数常规数值计算中,返回值均为双精度浮点数(IEEE 754 规范)。值得注意的是,当计算发生除以 0、无效转换或操作数本身包含 NaN 时,返回结果是特殊值 NaN,而 typeof NaN 在语言规范中依然属于 number。
2. bigint 类型返回值
自 ES2020(ES11)起,JavaScript 原生支持了大整数 BigInt。当取余运算符两端均为 BigInt 类型时,计算结果同样为 bigint 类型。但需要格外注意:JavaScript 禁止 BigInt 与 Number 进行隐式混合运算。
// 示例 3:BigInt 取余与类型隔离
const b1 = 10n;
const b2 = 3n;
const resBig = b1 % b2;
console.log(resBig, typeof resBig);
// 示例输出: 1n "bigint"
// 错误示例:混用不同类型
// const err = 10n % 3; // 运行时抛出 TypeError: Cannot mix BigInt and other types
3. 隐式类型转换(ToNumeric 抽象操作)
若操作数不是纯数值类型,JavaScript 引擎会首先调用内部的 ToNumeric 抽象操作将操作数强制转换为数值后再进行计算:
- 字符串:纯数字字符串自动转换为对应数字(如
"10" % "3" === 1);包含非数字字符的字符串转换为NaN,导致整体结果为NaN(如"10a" % 3 === NaN)。 - 布尔值:
true转为1,false转为0(如true % 2 === 1,false % 2 === 0)。 - null 与 undefined:
null转换为0;undefined转换为NaN。
四、边界情况与浮点数精度处理
取余运算在面对数学边界与浮点数时,有若干需要特别留意的规范要求:
1. 除以 0 的计算表现
在基础除法 / 中,非零数字除以 0 会得到正无穷 Infinity 或负无穷 -Infinity。但在取余运算中,任何非零有限数对 0 取余,结果均直接为 NaN:
// 示例 4:除以 0 的对比
console.log(5 / 0); // Infinity
console.log(5 % 0); // NaN (类型为 number)
console.log(0 % 0); // NaN
// 示例输出: Infinity, NaN, NaN
另外,对于 BigInt 而言,10n % 0n 会直接抛出运行时异常 RangeError: Division by zero,而不是返回 NaN。
2. 涉及 Infinity 的取余运算
- 被除数为无穷大:
Infinity % x结果恒为NaN。 - 除数为无穷大:
x % Infinity(其中 x 为有限数值),结果直接等于被除数x本身。因为商数为 0,余下所有被除数。
3. 浮点数取余的精度损耗
JavaScript 的所有 Number 均基于 IEEE 754 双精度 64 位二进制浮点数表示。某些十进制小数在二进制下是无限循环小数,因此在进行取余时可能会产生微小的舍入偏差:
// 示例 5:浮点数取余的精度误差
console.log(5.5 % 2); // 1.5 (整除部分精度准确)
console.log(0.3 % 0.1); // 期望为 0,实际为 0.09999999999999998
// 示例输出: 1.5, 0.09999999999999998
工程解决方案:在涉及精确小数场景(如货币金额、百分比刻度换算)时,应先将浮点数乘以 10^n 放大为整数进行取余,随后再将结果除以 10^n 还原,或借助定点数算法规避。
五、典型工程应用场景与实战模式
取余运算符在日常前端及后端逻辑开发中具有高频的实用价值。以下总结四个最常用的工程模式:
1. 环形数组与轮播索引越界防护
在实现无缝轮播图、走马灯或环形任务队列时,经常需要让索引在到达末尾后自动折返到第 0 位。使用 % 可以写出非常简洁的步进代码:
const list = ["Card-A", "Card-B", "Card-C"];
let index = 0;
// 连续点击“下一张”
function next() {
index = (index + 1) % list.length;
return list[index];
}
// 连续点击“上一张”(通过加模修正避免出现负索引)
function prev() {
index = (index - 1 + list.length) % list.length;
return list[index];
}
2. 时间度量与多级单位拆解
把接口返回的纯秒数格式化展示为标准的 HH:mm:ss 形式:
function formatDuration(totalSeconds) {
const s = totalSeconds % 60;
const m = Math.floor(totalSeconds / 60) % 60;
const h = Math.floor(totalSeconds / 3600);
const pad = (n) => String(n).padStart(2, "0");
return `${pad(h)}:${pad(m)}:${pad(s)}`;
}
console.log(formatDuration(3665));
// 示例输出: "01:01:05"
3. 二维网格行列坐标映射
当用一维数组存储多行多列的网格数据时,通过一维索引 i 和列数 cols 可以直接反推其所在的行号与列号:
const cols = 4; // 每行 4 列
const itemIndex = 7;
const rowIndex = Math.floor(itemIndex / cols); // 第 1 行 (从0计数)
const colIndex = itemIndex % cols; // 第 3 列
console.log(`行: ${rowIndex}, 列: ${colIndex}`);
// 示例输出: 行: 1, 列: 3
六、核心规则速查清单
| 表达式 | 计算结果 | 返回值类型 | 底层依据与说明 |
|---|---|---|---|
7 % 3 |
1 |
number |
标准正数相除向零截断 |
-7 % 3 |
-1 |
number |
结果符号严格由被除数(左操作数)决定 |
7 % -3 |
1 |
number |
除数符号对余数正负无影响 |
10n % 3n |
1n |
bigint |
BigInt 专属大整数运算,维持 bigint 类型 |
10n % 3 |
抛出 TypeError |
无 | 禁止 BigInt 与 Number 隐式混合计算 |
5 % 0 |
NaN |
number |
有限数对 0 取余在 JS 规范中返回 NaN |
5.5 % 2 |
1.5 |
number |
支持浮点数取余(需注意二进制舍入误差) |
"6" % 4 |
2 |
number |
字符串 "6" 经 ToNumeric 强转为数字 6 |
免登录快速分享代码:适合面试协作的轻量级JS在线运行工具推荐
2026-11-05 14:45:59
JS空字符串和纯空格转浮点数的结果是什么
2026-11-05 11:04:42
JS在线格式化缩进混乱怎么办?配置项调整技巧
2026-11-04 23:27:25
JS怎么获取秒级时间戳
2026-11-03 21:45:29
JS时间戳怎么从毫秒转换为秒
2026-11-03 20:04:14








