JavaScript的百分号(模运算符)对负数进行运算会得到一个负数结果。

376
根据Google计算器(-13) % 64的结果是51
根据Javascript(请参见JSBin),结果为-13
我该如何解决这个问题?

这可能只是一个优先级问题。你是指 (-13) % 64 还是 -(13 % 64)?个人而言,我会两种情况都加上括号,以增加清晰度。 - Tyler
4
尽管这是一个 JavaScript 问题,但本质上它与“Java 如何对负数进行取模运算?”这个问题非常相似。 - President James K. Polk
24
在 JavaScript 中,根本问题在于百分号(%)不是模运算符,而是余数运算符。JavaScript 中没有模运算符。因此,接受的答案是可行的方法。 - Redu
3
你是想修复哪个,Google 还是 JS? - Arnaud
6
为什么几乎没有语言实现取模操作,考虑到它是多么有用? - Arnaud
13个回答

373

33
我不确定是否应该称它为“错误”。取模运算在负数上的定义不是很明确,不同的计算环境会有不同的处理方式。维基百科关于「取模运算」的文章涵盖了这个问题。请参考该文章。 - Daniel Pryden
34
因为它通常被称为“模运算”,暗示它的行为与其数学定义相同(请参见ℤ/nℤ代数),所以这种看法似乎很愚蠢,但实际上并非如此。 - etienne
9
为什么要在加n之前取模?为什么不是先加n再取模? - starwed
21
如果不使用这个%n,当x < -n时会失败,比如(-7 + 5) % 5 === -2,但是((-7 % 5) + 5) % 5 == 3。请注意,%在这里表示取模运算。 - fadedbee
14
建议在答复中加入这样一句话:访问此功能应使用格式 (-13).mod(10) 而非 -13 % 10。这样更清晰明了。 - Jp_
显示剩余15条评论

251

使用 Number.prototype 是慢的,因为每次使用原型方法时都会将您的数字包装在一个 Object 中。不要这样做:

Number.prototype.mod = function(n) {
  return ((this % n) + n) % n;
}

使用:

function mod(n, m) {
  return ((n % m) + m) % m;
}

查看:https://jsperf.app/negative-modulo/2

比使用prototype快约97%。当然,如果性能对您很重要的话..


1
很好的提示。我使用了你的jsperf并将其与此问题中的其他解决方案进行了比较(但似乎这仍然是最好的):http://jsperf.com/negative-modulo/3 - Mariano Desanze
23
微优化。除非进行了大量的模运算,否则这样做不会有任何区别。编写最清晰、最易于维护的代码,然后在性能分析后进行优化。 - ChrisV
我认为你在第二个例子中搞错了 nm 的顺序,@StuR。应该是 return ((n % m) + m) % m; - OdinX
32
这个回答中提到的动机是一种微小的优化,但修改原型具有问题。更好的做法是选择副作用最少的方法,也就是这个方法。 - Keen
5
@JeneralJames修改原型的主要问题在于命名空间冲突。归根结底,这只是全局数据的变异。在不考虑小型一次性代码的情况下,改变全局数据是不好的实践。将函数导出为可跟踪的依赖项。特例的填充程序在这里无关紧要。这不是一个填充程序。真正的填充程序遵循使冲突安全的标准。如果你想在原则上进行争论,有一个单独的问题可以用来讨论它。https://dev59.com/HG025IYBdhLWcg3wFxya - Keen
显示剩余5条评论

40
在JavaScript中,% 运算符是一个求余运算符,而不是模运算符(主要区别在于如何处理负数):

-1 % 8 // -1, 不是 7


13
它应该被称为余数运算符,但它被称为模运算符:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_Operators#Arithmetic_operators - Big McLargeHuge
21
@DaveKennedy说,MDN不是官方的语言参考资料,它是一个由社区编辑的网站,有时会出现错误。根据规范,它并没有将其称为模运算符,据我所知,它从未这样称呼过(我查阅了ES3)。规范明确表示该运算符产生隐含除法的余数,并仅称之为“%运算符”。 - T.J. Crowder
4
如果被称为“余数”,根据定义它必须大于0。你还记得高中的“除法定理”吗?也许你可以在这里看一下:https://en.wikipedia.org/wiki/Euclidean_division - Ahmad
@Ahmad,现在它被称为 multiplicative operator - RobG
8
“mod”应该从一开始就被实现在每种编程语言中。在编程30年后,我从来没有需要过 a 是负数时的 a % b 操作:我每次需要的都是 mod(a,b)。 - Arnaud

20

一个"mod"函数用于返回正值。

var mod = function (n, m) {
    var remain = n % m;
    return Math.floor(remain >= 0 ? remain : remain + m);
};
mod(5,22)   // 5
mod(25,22)  // 3
mod(-1,22)  // 21
mod(-2,22)  // 20
mod(0,22)   // 0
mod(-1,22)  // 21
mod(-21,22) // 1

当然,

mod(-13,64) // 51

1
抱歉,您指定的链接实际上在URL中引用了#sec-applying-the-mod-operator :) 无论如何,感谢您的提醒,我已经从我的答案中去掉了冗余内容,因为它并不重要。 - Shanimal
4
哈哈!是的,这是HTML编辑器犯了错误。但规范文本没有错。 - T.J. Crowder

12

被采纳的答案让我有点紧张,因为它重复使用了 % 运算符。如果JavaScript将来更改了行为会怎么样?

这里是一个解决方法,它不会重复使用 %:

function mod(a, n) {
    return a - (n * Math.floor(a/n));
}

mod(1,64); // 1
mod(63,64); // 63
mod(64,64); // 0
mod(65,64); // 1
mod(0,64); // 0
mod(-1,64); // 63
mod(-13,64); // 51
mod(-63,64); // 1
mod(-64,64); // 0
mod(-65,64); // 63

14
如果 JavaScript 将取模运算符修改为与数学定义相匹配,那么被接受的答案仍将有效。 - starwed
30
"如果JavaScript将来改变了行为怎么办?" - 为什么会呢?改变这样一个基本运算符的行为是不太可能的。 - nnnnnn
2
+1 分享这个关注点和对 #answer-4467559 的替代方案,有 4 个原因:(1) 虽然它声明,“更改如此基本的操作的行为不太可能”,但仍然值得考虑,即使是为了发现它不需要。(2) 在一个损坏的操作中定义一个工作操作,虽然令人印象深刻,但至少在第一眼看起来令人担忧,直到证明不是这样。(3) 尽管我还没有很好地验证这个替代方案,但我发现它更容易跟随快速查看。(4) 微小:它使用 1 个 div+1 个 mul,而不是 2 个 (mod) divs,我听说在早期硬件上没有良好的 FPU,乘法更快。 - Destiny Architect
2
@DestinyArchitect 这不仅不明智,而且毫无意义。如果他们改变了余数运算符的行为,那么将会破坏使用它的大量程序。这是永远不会发生的。 - Aegis
26
如果 -*/;.(),Math.floorfunctionreturn 的行为发生变化,那么你的代码将会出现严重问题。请注意,不要改变原意。 - xehpuk
显示剩余3条评论

8
如果n是2的幂次方,那么当x是整数时,可以使用x & (n - 1)代替x % n
> -13 & (64 - 1)
51 

8

修复负数取模(余数运算符%)的问题

使用 ES6 箭头函数进行简化,并且不会危险地扩展 Number 原型

const mod = (n, m) => (n % m + m) % m;

console.log(mod(-90, 360));    //  270  (Instead of -90)


5
尽管它的表现并不符合您的预期,但这并不意味着JavaScript没有“行为”。这是JavaScript为其模数计算所做的选择。因为根据定义,任何答案都有意义。 请参见维基百科上的链接。您可以在右侧看到不同语言选择结果标志的方式。

4

这不是一个bug,有三个函数可以计算模数,您可以使用适合自己需求的函数(我建议使用欧几里得函数)

截断小数部分函数

console.log(  41 %  7 ); //  6
console.log( -41 %  7 ); // -6
console.log( -41 % -7 ); // -6
console.log(  41 % -7 ); //  6

整数部分函数

Number.prototype.mod = function(n) {
    return ((this%n)+n)%n;
};

console.log( parseInt( 41).mod( 7) ); //  6
console.log( parseInt(-41).mod( 7) ); //  1
console.log( parseInt(-41).mod(-7) ); // -6
console.log( parseInt( 41).mod(-7) ); // -1

Euclidean function

Number.prototype.mod = function(n) {
    var m = ((this%n)+n)%n;
    return m < 0 ? m + Math.abs(n) : m;
};

console.log( parseInt( 41).mod( 7) ); // 6
console.log( parseInt(-41).mod( 7) ); // 1
console.log( parseInt(-41).mod(-7) ); // 1
console.log( parseInt( 41).mod(-7) ); // 6

2
在欧几里得函数中检查 m < 0 是无用的,因为 ((this%n)+n)%n 总是正数。 - bormat
1
@bormat 是的,但在Javascript中,可能会返回负结果(这就是这些函数的目的,用于修复它)。 - zessx
你写了这段代码:[code] Number.prototype.mod = function(n) { var m = ((this%n)+n)%n; return m < 0 ? m + Math.abs(n) : m; }; [/code]请给我一个m为负数的n值。不存在任何使m成为负数的n值,因为在第一个%之后,你添加了n。 - bormat
没有这个检查,parseInt(-41).mod(-7) 将返回 -6 而不是 1(这正是我编写整数部分函数的目的)。 - zessx
1
你可以通过移除第二个模数来简化函数 Number.prototype.mod = function(n) { var m = this%n;
return (m < 0) ? m + Math.abs(n) : m;
};
- bormat
显示剩余3条评论

2

看起来如果你想在度数上进行修改(例如,如果你有-50度至200度),你需要使用类似以下的内容:

function modrad(m) {
    return ((((180+m) % 360) + 360) % 360)-180;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接