前言
项目中,总是不可避免会出现js的浮点数精度计算问题。
抛出经典问题:为什么0.1===0.1
,0.1+0.2!==0.3
出现问题的原因
根源要追溯到JavaScript的浮点数的存储上。和其它语言如Java和Python不同,JavaScript中所有数字包括整数和小数都只有一种类型 — Number。它的实现遵循 IEEE 754 标准,使用64位固定长度来表示,也就是标准的 double 双精度浮点数(相关的还有float 32位单精度)。
这样的存储结构优点是可以归一化处理整数和小数,节省存储空间。64位比特又可分为三个部分:
- 符号位S:第 1 位是正负数符号位(sign),0代表正数,1代表负数
- 指数位E:中间的 11 位存储指数(exponent),用来表示次方数
-
尾数位M:最后的 52 位是尾数(mantissa),超出的部分自动进一舍零
图片.png
在了解了存储的原理之后可能还不太理解。通过案例应该可以更加具体一点。
在执行计算0.1+0.2的过程中发生了什么?
因为计算机内部只能通过二级制处理数据,0.1和0.2会先被转换成二进制,但是由于浮点数用二进制表示时是无穷的,因此会被转换为:
0.1 -> 0.0001 1001 1001 1001...(1001循环)
0.2 -> 0.0011 0011 0011 0011...(0011循环)
相加之后就是0.0100 1100 ...(1100循环)
前面提到了,JavaScript提供的有效数字最长为53个二进制位(64位浮点的后52位+有效数字第一位的1)。最后的 52 位尾数超出部分发生了截断,因此,0.1+0.2的计算结果在截断后,二进制转换回十进制,得到的结果是0.30000000000000004
整数精度问题
前面提到了,既然小数存在,那整数也存在着上限。
js的Number自带存储了最大/最小安全整数
Number.MAX_SAFE_INTEGER === 9007199254740991
(2^53 - 1)
Number.MIN_SAFE_INTEGER === -9007199254740991
-(2^53 - 1)
至于为什么不是52次方-1。对于二进制来说, 小数点前保留一位, 规格化后始终是 1.***, 节省了 1 bit,这个 1 并不需要保存,所以可以多展示1位数,即2^53-1。
那么超出之后进行运算会发生什么就不用再解释了,原理同浮点数是一样的。
解决方案
除了自己造轮子的运算中进位,更推荐使用Math.js、decimal.js、big.js等类库,其中有完善的解决方案,具体想了解实现原理可以移步官方git直接查看函数方法。
-
Math.js
Math.js 是专门为 JavaScript 和 Node.js 提供的一个广泛的数学库。它具有灵活的表达式解析器,支持符号计算,配有大量内置函数和常量,并提供集成解决方案来处理不同的数据类型
像数字,大数字(超出安全数的数字),复数,分数,单位和矩阵。 功能强大,易于使用。
官网:http://mathjs.org/
GitHub:https://github.com/josdejong/mathjs -
decimal.js
为 JavaScript 提供十进制类型的任意精度数值。
官网:http://mikemcl.github.io/decimal.js/
GitHub:https://github.com/MikeMcl/decimal.js -
big.js
官网:http://mikemcl.github.io/big.js
GitHub:https://github.com/MikeMcl/big.js/
不过通常不需要引入整个库,只需要其中的几个方法就适用了,例如常用的保留小数后几位(基于parseFloat和tofixed去改造……)
心得体会
前端的浮点数问题并不会太复杂,因为涉及到业务数据的计算通常都是交给后端完成,但是精确位数尤其是金额的展示还是不能出错。虽然涉及到计算机组成原理的知识,其实还是很容易弄懂的。
网友评论