这是一个 2010 年的 bug:Math.round rounds incorrectly.
这个 bug 很有意思,因为引擎按照 ES 规范实现了功能,结果发现规范给出的实现,根本实现不了规范。(好拗口)
在 ES5 规范中(15.8.2.15) 对于 Math.round(x) 的定义:
... If
xis greater than0but less than0.5, the result is+0. ...
如果 x 大于 0 但是小于 0.5,那么结果是 +0。
在这节后面还有个备注:
The value of
Math.round(x)is the same as the value ofMath.floor(x+0.5), except whenxisc−0or is less than0but greater than or equal to-0.5; for these casesMath.round(x)returns−0, butMath.floor(x+0.5)returns+0.
Math.round(x) 的返回值与 Math.floor(x+0.5) 的返回值相同。只有一种例外:当 x 为 −0 或 x 小于 0且大于等于 -0.5 时, Math.round(x) 返回 −0, 但 Math.floor(x+0.5) 返回 +0。
那么问题来了,比如 0.499999999999999944:
0.499999999999999944 < 0.5 // true
可以看到这个值小于 0.5,所以 Math.round(0.499999999999999944) 的结果应该是 0。但是之前的主流引擎都使用备注里面的描述实现的,先计算: x+0.5,然后对结果应用 Math.floor:
0.499999999999999944 + 0.5 // 1Math.floor(0.499999999999999944 + 0.5) // 1
本来结果应该是 0,结果按照规范得出了错误的结果 1。
于是在 ES6 规范中(20.2.2.28) 这个备注修改为了:
The value of
Math.round(x)is not always the same as the value ofMath.floor(x+0.5). Whenxis−0or is less than0but greater than or equal to-0.5,Math.round(x)returns−0, butMath.floor(x+0.5)returns+0.Math.round(x)may also differ from the value ofMath.floor(x+0.5)because of internal rounding when computingx+0.5.
Math.round(x) 的返回值并不总是等于 Math.floor(x+0.5)。最后一句解释了原因,由于 x+0.5 在计算过程中可能进行内部舍入。
除了这个,还有一个数会被四舍五入到 1:
Math.round(0.49999999999999999) === 1
这个不是 bug,因为这个看似比 0.5 小的数并不比 0.5 小:
0.49999999999999999 === 0.5 // true
而我们用来在上面举例的数:
0.49999999999999994 === 0.499999999999999944 // true0.49999999999999994 === 0.49999999999999995 // true0.49999999999999994 === 0.49999999999999996 // true0.49999999999999994 === 0.49999999999999997 // true0.49999999999999994 === 0.49999999999999993 // true0.49999999999999994 === 0.49999999999999992 // true
一定要注意浮点数的舍入。
除此之外, Math.floor(x+0.5) 还有一个潜在问题,那就是在计算 x+0.5 时,如果 x 非常大,也会出现 x+0.5 舍入到 x+1 的情况,比如 2**52:
2**52 + 1 // 45035996273704974503599627370497 + 0.5 // 4503599627370498
我们知道 JavaScript 的最大安全整数 Number.MAX_SAFE_INTEGER 是 2**53-1,所以如果 Math.round(4503599627370497) 得到了 4503599627370498 那肯定是个 bug。
V8 通过差值和 0.5 比较然后进行 -1.0 调整:Issue 567011: Fix a bug that Math.round() returns incorrect results for huge integers
static Object* Runtime_Math_round(Arguments args) {NoHandleAllocation ha;ASSERT(args.length() == 1);CONVERT_DOUBLE_CHECKED(x, args[0]);if (signbit(x) && x >= -0.5) return Heap::minus_zero_value();double integer = ceil(x);if (integer - x > 0.5) { integer -= 1.0; }return Heap::NumberFromDouble(integer);}
而 Firefox 则通过直接检查指数的方式:
double js::math_round_impl(double x){AutoUnsafeCallWithABI unsafe;int32_t ignored;if (NumberIsInt32(x, &ignored))return x;/* Some numbers are so big that adding 0.5 would give the wrong number. */if (ExponentComponent(x) >= int_fast16_t(FloatingPoint<double>::kExponentShift))return x;double add = (x >= 0) ? GetBiggestNumberLessThan(0.5) : 0.5;return js_copysign(fdlibm::floor(x + add), x);}
参考文章
Math.round() - MDN
JavaScript's Tricky Rounding
Standard ECMA-262 5.1 Edition
Standard ECMA-262 6 Edition