这是一个 2010 年的 bug:Math.round rounds incorrectly.
这个 bug 很有意思,因为引擎按照 ES 规范实现了功能,结果发现规范给出的实现,根本实现不了规范。(好拗口)
在 ES5 规范中(15.8.2.15) 对于 Math.round(x)
的定义:
... If
x
is greater than0
but 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 whenx
isc−0
or is less than0
but 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 // 1
Math.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)
. Whenx
is−0
or is less than0
but 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 // true
0.49999999999999994 === 0.49999999999999995 // true
0.49999999999999994 === 0.49999999999999996 // true
0.49999999999999994 === 0.49999999999999997 // true
0.49999999999999994 === 0.49999999999999993 // true
0.49999999999999994 === 0.49999999999999992 // true
一定要注意浮点数的舍入。
除此之外, Math.floor(x+0.5)
还有一个潜在问题,那就是在计算 x+0.5
时,如果 x
非常大,也会出现 x+0.5
舍入到 x+1
的情况,比如 2**52
:
2**52 + 1 // 4503599627370497
4503599627370497 + 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