——从面试题 「判断 0.1 + 0.2 与 0.3 是否相等?」说开去
底层实现
由于JS在存储数据的时候采取的是IEEE754标准。所以数据是在底层是用64为01串表示的,其中1位为符号位S、11位为指数位E、52位为尾数位M(全部表示小数,默认整数部分为1)。数据与01串的转化关系如下图:
其中,最多有53位都表示1则底数最大为1.11..1(共53个),指数最大为E=2^11,因此上述公式表示的数值最大的情况是:
0.1+0.2
- 0.1转化为2进制的数为:
则其表示成指数的形式为:
在存储0.1的过程中因为尾数最多只有52位因此造成了精度的损失。
2. 0.2转化为2进制的数为:
则其表示成指数的形式为:
与0.1相同,在存储0.2的过程中也因为尾数的位数限制造成了精度的损失。
3. 按位相加
(注:相加时有进位,但是只能存52位+1,因此四舍五入了)
转化为十进制为:
=0.30000000000000004440892098500626
由于存储和计算过程中的精度损失导致了0.1+0.2!==0.3
安全数
52位(+1)的尾数位数的限制除了会导致某些小数的运算精度不准确还会导致大数问题.
案例
在JS编码过程中,表格里有一个加入用户群的功能。前端通过后端返回的Json串读取相应的字段,展现表格的数据,当点击对应的按钮时,将该行对应的用户群id返回给后端做加群的请求。
在很长一段时间都没有出问题,直到有一天,用户点击新的行数据时后端再也不返回200了。
通过模拟整个操作流程确定了问题的所在。随着用户群的增多,表示用户群的数值的位数渐渐变大。而当后端返回的用户群id为「7893645297283646558」在浏览器输出变成了「7893645297283646000」,后端理所当然找不到对应的ID。
JS读取规则
出现这样的转化过程是因为JS在从内存中读取一个数的时候,正数最多只保存16位有效数字,小数最多保留17位,多余位数置0.
7893645297283646558
实际上这种位数过大的数,在JS中并不安全,因为这个数可能并不能在内存中表示出来。
安全数
安全数,指的是在内存中可以准确表达的数,此范围内的数可以正确的进行加减等运算。
回到IEEE标准:
指数决定最大最小值,尾数决定数据的精度。尾数的最大值为1.1...1因此,最大值为1.1...11*2^x,即:
因此最大安全数是:
(因为 任意数*2 都存在2的间隔,并不能+1)
9007199254740992
疑惑
都是双精度浮点型,为什么python就可以正常传递数据而JS就会出现问题呢?
因为python用的是长整型,不存在溢出问题,即可以存放任意大小的整数。而长整型在python内部是用一个 int 数组保存值的。
解决办法
- 最常见的方法就是将小数转化为整数
- 将大数存储成字符串/数组,再进行相应的运算
- BigInt()(后缀为n) 但是BigInt类型的数据只能与BigInt类型的数据做运算,不支持小数的转换
a
- 使用封装好的库
- Math.js 可以进行含单位的运算,
- decimal.js 为 JavaScript 提供十进制类型的任意精度数值
- bignumber(new BigNumber(xxx))本质上也是字符串,性能可能没有原生的好