尴尬:ES 规范对于 Math.round 的定义,根本实现不了规范

这是一个 2010 年的 bug:Math.round rounds incorrectly.

这个 bug 很有意思,因为引擎按照 ES 规范实现了功能,结果发现规范给出的实现,根本实现不了规范。(好拗口)

在 ES5 规范中(15.8.2.15) 对于 Math.round(x) 的定义:

... If x is greater than 0 but less than 0.5, the result is +0. ...

如果 x 大于 0 但是小于 0.5,那么结果是 +0

在这节后面还有个备注:

The value of Math.round(x) is the same as the value of Math.floor(x+0.5), except when x isc 0 or is less than 0 but greater than or equal to -0.5; for these cases Math.round(x) returns 0, but Math.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

 
 
  1. 0.499999999999999944 < 0.5 // true

可以看到这个值小于 0.5,所以 Math.round(0.499999999999999944) 的结果应该是 0。但是之前的主流引擎都使用备注里面的描述实现的,先计算: x+0.5,然后对结果应用 Math.floor

 
 
  1. 0.499999999999999944 + 0.5  // 1

  2. 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 of Math.floor(x+0.5). When x is 0 or is less than 0 but greater than or equal to -0.5Math.round(x) returns 0, but Math.floor(x+0.5) returns +0Math.round(x) may also differ from the value of Math.floor(x+0.5) because of internal rounding when computing x+0.5.

Math.round(x) 的返回值并不总是等于 Math.floor(x+0.5)。最后一句解释了原因,由于 x+0.5 在计算过程中可能进行内部舍入。

除了这个,还有一个数会被四舍五入到 1

 
 
  1. Math.round(0.49999999999999999) === 1

这个不是 bug,因为这个看似比 0.5 小的数并不比 0.5 小:

 
 
  1. 0.49999999999999999 === 0.5 // true

而我们用来在上面举例的数:

 
 
  1. 0.49999999999999994 === 0.499999999999999944 // true

  2. 0.49999999999999994 === 0.49999999999999995  // true

  3. 0.49999999999999994 === 0.49999999999999996  // true

  4. 0.49999999999999994 === 0.49999999999999997  // true

  5. 0.49999999999999994 === 0.49999999999999993  // true

  6. 0.49999999999999994 === 0.49999999999999992  // true

一定要注意浮点数的舍入。

除此之外, Math.floor(x+0.5) 还有一个潜在问题,那就是在计算 x+0.5 时,如果 x 非常大,也会出现 x+0.5 舍入到 x+1 的情况,比如 2**52

 
 
  1. 2**52 + 1 // 4503599627370497

  2. 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

 
 
  1. static Object* Runtime_Math_round(Arguments args) {

  2.  NoHandleAllocation ha;

  3.  ASSERT(args.length() == 1);

  4.  CONVERT_DOUBLE_CHECKED(x, args[0]);

  5.  if (signbit(x) && x >= -0.5) return Heap::minus_zero_value();

  6.  double integer = ceil(x);

  7.  if (integer - x > 0.5) { integer -= 1.0; }

  8.  return Heap::NumberFromDouble(integer);

  9. }

而 Firefox 则通过直接检查指数的方式:

 
 
  1. double js::math_round_impl(double x)

  2. {

  3.    AutoUnsafeCallWithABI unsafe;

  4.    int32_t ignored;

  5.    if (NumberIsInt32(x, &ignored))

  6.        return x;

  7.    /* Some numbers are so big that adding 0.5 would give the wrong number. */

  8.    if (ExponentComponent(x) >= int_fast16_t(FloatingPoint<double>::kExponentShift))

  9.        return x;

  10.    double add = (x >= 0) ? GetBiggestNumberLessThan(0.5) : 0.5;

  11.    return js_copysign(fdlibm::floor(x + add), x);

  12. }

参考文章

  • Math.round() - MDN

  • JavaScript's Tricky Rounding

  • Standard ECMA-262 5.1 Edition

  • Standard ECMA-262 6 Edition

640?wx_fmt=png



  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值