我们用JS进行浮点数运算时,比如0.1 + 0.2,经常会得到一个意想不到的结果:
console.log(0.1 + 0.2) // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3) // false
这个结果是不是很匪夷所思,那么是什么造成这个结果的呢?
原理分析
简单来说就是因为:EcmaScrpt规范定义,JavaScript中所有的数字(包括整数和小数)都只有一种类型–Number。而Number的实现遵循IEEE 754标准,使用64位固定长度来表示,也就是标准的double双精度浮点数。它的优点是可以归一化处理整数和小数,节省储存空间。而实际计算的时候会转换成二进制计算再转成十进制。进制转换之后会很长,舍去一部分,计算再转回来,就有了精度误差。
事实上,这个问题并不只出现在JS里,其他遵循IEEE 754标准的语言,同样存在此问题。
解决方案
这个问题既然已经存在很长时间了,那么解决方案自然也就有很多了。
- 官方方案
TC39 已经有一个 Stage 3 的提案 proposal bigint,大数问题有望彻底解决。在浏览器正式支持前,可以使用 Babel 7.0 来实现,它的内部是自动转换成 big-integer 来计算,这样能保持精度但运算效率会降低。
1.1 主动缩小精度范围
利用toPrecision或toFixed主动把溢出造成的细微差异过滤掉。这种方案吧,只适合浮点数,对于整数的溢出,是束手无策的。
1.2 转为整数计算
这种方案跟上面的也差不多,只适合小数。对于包含n位小数的number,先乘10的n次方,再进行计算,最后结果再除10的n次方归位
1.3 转为字符串再进行计算
这种应该是目前最普遍的解决方案了,大多数成熟计算框架都采用的此方案。优点是完全不受number存储空间的限制,可以胜任更准确更精细的计算,缺点是性能相对较差。
1.4 借助第三方计算框架
1.4.1 math.js
Math.js是一个用于JavaScript和Node.js的扩展数学库。它具有支持符号计算的灵活表达式解析器,大量内置函数和常量,并提供了集成的解决方案来处理不同的数据类型,例如数字,大数,复数,分数,单位和矩阵。强大且易于使用。
使用方法:
// 函数和常量
math.round(math.e, 3) // 2.718
math.atan2(3, -3) / math.pi // 0.75
math.log(10000, 10) // 4
math.sqrt(-4) // 2i
math.derivative('x^2 + x', 'x') // 2*x+1
math.pow([[-1, 2], [3, 1]], 2)
// [[7, 0], [0, 7]]
// 表达式
math.evaluate('1.2 * (2 + 4.5)') // 7.8
math.evaluate('12.7 cm to inch') // 5 inch
math.evaluate('sin(45 deg) ^ 2') // 0.5
math.evaluate('9 / 3 + 2i') // 3 + 2i
math.evaluate('det([-1, 2; 3, 1])') // -7
// 链接
math.chain(3)
.add(4)
.multiply(2)
.done() // 14
1.4.2 decimal.js
此库 与 bignumber.js相似,但是注意此库的精度是以 有效数字 来规定的,而不是小数点的位置. 此库中所有的运算都是四舍五入到 对应的精度(类似于python中的 decimal 模块),而不是仅仅只是除法运算四舍五入
使用方法
x = new Decimal(9) // '9'
y = new Decimal(x) // '9'
new Decimal('5032485723458348569331745.33434346346912144534543')
new Decimal('4.321e+4') // '43210'
new Decimal('-735.0918e-430') // '-7.350918e-428'
new Decimal('5.6700000') // '5.67'
new Decimal(Infinity) // 'Infinity'
new Decimal(NaN) // 'NaN'
new Decimal('.5') // '0.5'
new Decimal('-0b10110100.1') // '-180.5'
new Decimal('0xff.8') // '255.5'
new Decimal(0.046875) // '0.046875'
new Decimal('0.046875000000') // '0.046875'
new Decimal(4.6875e-2) // '0.046875'
new Decimal('468.75e-4') // '0.046875'
new Decimal('0b0.000011') // '0.046875'
new Decimal('0o0.03') // '0.046875'
new Decimal('0x0.0c') // '0.046875'
new Decimal('0b1.1p-5') // '0.046875'
new Decimal('0o1.4p-5') // '0.046875'
new Decimal('0x1.8p-5') // '0.046875'
1.4.3 bignumber.js
bignumber.js是一款用于任意精度十进制和非十进制算术的JavaScript库
使用方法
var BigNumber = require('big-number');
BigNumber(5)
.plus(97)
.minus(53)
.plus(434)
.multiply(5435423)
.add(321453)
.multiply(21)
.div(2)
.pow(2)
// 760056543044267246001
1.4.4 big.js
?Big.js是JavaScript的”bignumber”实现。这意味着您可以使用它以任何精度存储任何量级的数字,并在 JavaScript 中对它们执行数学运算。?
使用方法
x = new Big(9) // '9'
y = new Big(x) // '9'
new Big('5032485723458348569331745.33434346346912144534543')
new Big('4.321e+4') // '43210'
new Big('-735.0918e-430') // '-7.350918e-428'
Big(435.345) // '435.345'
new Big() // 'Error: [big.js] Invalid value'
Big() // No error, and a new Big constructor is returned