双精度浮点数表示法

JavaScript 数字采用的是 IEEE-754 标准 的双精度浮点数表示法。双精度存储占用 64bit,其中尾数占 52bit,这决定了它能表示的最大安全整数为 253-1。所以说,JavaScript 能够准确表示的整数范围在 -253 到 253 之间(不含两个端点),超过这个范围就无法精确表示,比如:

2**53               // 9007199254740992
9007199254740992    // 9007199254740992
9007199254740993    // 9007199254740992

事实上,出现这个问题的几率极小,毕竟很少有业务需要用到超大整数。相对而言,另一个问题比它严重的多。另一个问题是,二进制浮点数表示法不能精确的表示十进制小数,比如十进制小数 0.1 转为二进制就成了 0.0001100110011…(无限循环)。因此,JavaScript 在进行小数计算的时候会出现一些问题,比如:

.2 - .1 // 0.1
.3 - .2 // 0.09999999999999998
.4 - .3 // 0.10000000000000003

尽管计算结果非常接近最终值,但在某些特定场景下,比如进行重要的金融计算,这个问题就不容小觑了。

Number.prototype.toFixed() 方法的问题

toFixed() 方法使用定点数表示法来格式化一个数字,返回值为 String 类型。值得注意的是该方法的舍入法则并不是四舍五入,很多人对此有误解。而且,不同浏览器对 toFixed() 方法的处理结果是不同的。比如:(2.445).toFixed(2) 在 IE 中的执行结果为 "2.45",但是在 Chrome 中的执行结果为 "2.44"。曾经某商城出现不同浏览器上显示价格不一样的问题就是使用了 toFixed() 方法导致的。总的来说,toFixed() 方法不能用来处理这种小数舍入问题。

Math.round() 方法的问题

Math.round() 函数返回一个数字四舍五入后最接近的整数。不同于 toFixed() 方法,Math.round() 方法本身没有问题,你可以放心的使用它对一个数字进行四舍五入。但是实际场景中,我们往往需要处理的是计算结果而非字面量。问题就出在这个计算上,比如 2.445 * 100 的计算结果不是 244.5 而是 244.49999999999997,这个时候再使用 Math.round() 方法,其结果自然和预期的不一样了。

Math.round(244.5)       // 245
Math.round(2.445 * 100) // 244
2.445 * 100 === 244.5   // false

使用大整数进行计算来解决问题

或许 JavaScript 未来会支持十进制数字类型以避免这些问题。在这之前,我们需要使用大整数进行计算来解决问题。比如,要使用整数“分”而不是小数“元”进行基于货币单位的运算。下面例子演示将 2445 厘表示成 x.xx 元(四舍五入)。

(Math.round(2445 / 10) / 100).toFixed(2)    // "2.45"