小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
在前端开发,特别做电商领域应用,跟数字计算打交道可以说是家常便饭,数字计算毫无疑问是重中之重。购物车展示个错误的价格、错误的优惠折扣计算、后台订单总金额统计/报表数据分析等计算出问题,必然是致命的。
场景
下面看两个场景:
- 场景一
// 在chrome浏览器控制台输入:
0.1 + 0.2 === 0.3 // 输出:false
0.1 + 0.2 // 按回车输出: 0.30000000000000004
- 场景二
// 在chrome浏览器控制台输入:
9999999999999999 == 10000000000000000 // 输出: true
初次接触的时候,可能会对这迷惑行为,感到大跌眼镜,但对于有前端经验的人来说,知道这是精度问题以及数字越界了。
揭秘
解释两种场景,先了解下二进制。
在学计算机基础的时候,我们知道计算机内部是由集成电路这种电子部件构建成的,它的引脚只能支持0V或者5V两种状态,这种状态相对也决定计算机只能通过二进制数来处理数据,也就是0和1。同时,计算机处理信息的基础单位是:1个字节(一个字节等于8位二进制数)。(PS:二进制概念不是我们的重点,我们只需要知道计算机是根据二进制来处理数据即可)
而,JavaScript遵循的是[IEEE 754]
规范,因此采用的是双精度存储(double precision),占用 64 bit。用二进制表示如下图:
它表示的意义是:
- 第1位:用于表示符号位
- 第2-12位(共11位):用于表示指数
- 第13-64位(共52位):用于表示尾数
因此,我们可以得知,计算机描述数值是采用64位,扣除11位用于表示指数,所能描述的最大10进制数值为:Math.pow(2, 53)
,也就是值:9007199254740992
。同时,数字中存在一些无理数,它们是无穷的,换句话说,用计算机用有限位要表示无限的数值,就会存在精度的丢失问题,位数满了,只能按某种约定(四舍五入)处理数据。这里无限的数值准确的说是用二进制描述的数值,因此看似有穷的数值也可能对应的是无穷的二进制表示,比如场景一。
下面将场景一,按计算机的角度来执行看看:
// 输入:
0.1 + 0.2
// 数值转为二进制:变成无穷的二进制,但计算机用于表示的位数是有限的
0.1 >> 0.0001 1001 1001 1001 1001 ... (1001无限循环)
0.2 >> 0.0011 0011 0011 0011 0011 ... (0011无限循环)
// 10进制小数转为二进制计算法:数值✖️2 取进位,小数位继续✖️2,直至为0
0.2*2 = 0.4 0
0.4*2 = 0.8 0
0.8*2 = 1.6 1
0.6*2 = 1.2 1
0.2*2 = 0.4 0
场景二,超出Math.pow(2,53)
可能存在精度问题,涉及大数计算需谨慎
//
Math.pow(2,53) // 输出:9007199254740992
Math.pow(2,53)+1 // 输出:9007199254740992
Math.pow(2,53)+2 // 输出:9007199254740994
一目了然,豁然开朗了吗?
解决方案
解决数字计算精度、大数计算问题,可以自己封装库,或者采用三方库。这里推荐github上3.6K star的 big.js
库,因为它API简单易用、运行速度快、且压缩后大小只有6k。当然如果你想自己封装库也可以,比如:
- 当不会涉及特大数计算的时,只需考虑精度问题,解决思路可以是:将浮点数转为整数,计算完成再除以对应的倍数。
- 当涉及超大数时,简单思路,可将数值切割为数组进行反转,然后从低位往高位计算。
希望读完本文,能帮助你理解为啥有精度问题。