双精度浮点数表示法
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"