引言
非计算机领域的人看到这样的标题一定会觉得莫名其妙,0.1+0.2这样的算式和1+1=2差不多咋还能有其它结果。然而在计算机世界里确实存在很多类似的运算你觉得可以拍着胸脯保证结果但实际往往截然不同,归根究底都是因为精度丢失,我们今天就从JavaScript的角度来解读一下这个问题。
JavaScript的数值类型
不像Java、C语言有整型和浮点型(int、float、double),JavaScript只有一种数值类型:number
。和大部分编程语言一样number
也是基于IEEE 754标准实现的浮点数。
双精度浮点数
浮点数通过科学计数法存储以节约存储空间,JavaScript使用双精度格式,即64位二进制。科学计数法公式如下:
1位 | 11位 | 52位 |
---|---|---|
S(符号位),编号63 | E(阶码位),编号62 ~52 | M(尾数位),编号51 ~ 0 |
0表示正,1表示负 | 指数,可表示0~2047 | 超出部分舍0进1 |
为了能够处理负指数,实际指数位存储在指数域中的值需要减去一个偏移量(单精度为127,双精度为1023),因此指数0~1022表示负,1024~2047表示正。 而IEEE 754规定,有效数字第一位默认总是1,不保存在64位浮点数之中,所以尾数位最多可以存储53位有效数字。所以公式变为:
根据上面的公式我们来举个例子看看。一个十进制数10.5
,我们把它转成二进制得到:1010.1
,用科学计数法表示
1010.1 = 1.0101×2^3
因此S=0,E=1026,M=0101。
精度丢失
而像0.1这样的小数转成二进制是个无限循环:
0.0001100110011001100110011001100110011001100110011001101...
0.2转成二进制同样是无限循环:
0.001100110011001100110011001100110011001100110011001101...
由于尾数位有长度限制,无法精确存储,对正数来说,只要尾数多余位不全是0,则向尾数最低有效位进1;对负数来说,则是简单地舍去。
用科学计数法表示,0.1的阶码是-4,0.2的阶码是-3,想要对他们的尾数进行相加运算首先需要对阶,小阶对大阶,所以将0.1的阶码变为-3,尾数相应右移一位,因此:
1 | 0.1100110011001100110011001100110011001100110011001101(1100进1) |
用科学计数法表示阶码进1,尾数右移一位无法表示全,所以最低有效位需要进1,所以最后的结果就是:
2^-2×1.0011001100110011001100110011001100110011001100110100
将这个结果转成十进制刚好得到0.30000000000000004
。
解决精度计算
所以一切真相大白了,浮点运算的结果不准确就是由于精度丢失导致的,那么怎样避免?如果你做过和金额相关的业务,稍不留神一定会有测试给你提bug告诉你数值显示出错啦,怎么出现好多位小数。
简单的方法是先把小数转成整数再运算。
1 | function add(num1, num2) { |
方便的话,可以使用现成的工具库,比如:math.js。
部分内容参考自以下:
https://zhuanlan.zhihu.com/p/30703042
https://shenbao.github.io/2016/10/16/Javascript-0.1+0.2-!=-0.3/
https://blog.csdn.net/u013347241/article/details/79210840