为什么0.1+0.2不等于0.3,16位的正整数确等于17位?

487 阅读4分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

在前端开发,特别做电商领域应用,跟数字计算打交道可以说是家常便饭,数字计算毫无疑问是重中之重。购物车展示个错误的价格、错误的优惠折扣计算、后台订单总金额统计/报表数据分析等计算出问题,必然是致命的。

场景

下面看两个场景:

  • 场景一
// 在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。用二进制表示如下图:

二进制.png 它表示的意义是:

  • 第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。当然如果你想自己封装库也可以,比如:

  1. 当不会涉及特大数计算的时,只需考虑精度问题,解决思路可以是:将浮点数转为整数,计算完成再除以对应的倍数。
  2. 当涉及超大数时,简单思路,可将数值切割为数组进行反转,然后从低位往高位计算。

希望读完本文,能帮助你理解为啥有精度问题。