一篇读懂 Javascript 的浮点数精度问题
不看后悔系列,读完这篇文章,你将彻底理解 Javascript 是如何处理数字的,而不仅仅是浮点数的精度问题。
所有使用 Javascript 的开发者都应该要明白这背后的原理,因为写的多了这些问题一定会影响到你的代码。不仅如此,面试也有可能遇到,记得 LeetCode 里有一题需要把整数字符串转成整数,当数字很大的时候就会出现问题。
浮点数在内存里的样子
最近开发的产品需要解析字符串形式的算数表达式,然后计算得出结果,想必会遇到精度问题,就仔细研究了下底层原理,查阅了大量资料发现:大家用的都是同一个例子:
0.1 + 0.2
// 0.30000000000000004
但其实,是不需要算数运算的,很多浮点数都不精确。Javascript 只有一种数字类型,是遵循 IEEE754 标准的 64 位双精度二进制浮点数,其存储格式如下图:
双精度浮点数的内存格式
其中:
- sign:为正负符号标志位,记为 s
- exponent:为指数位,取值范围
[0, 2048)
,记为 e - fraction:为小数位,即有效数字,因为内存中以二进制存储,所以首位一定是1,因此省略以便能表示更多的数。
转十进制数的计算公式为:
转十进制计算公式
其中,b 为二进制小数的第 52 - i 位的值。e 需减去取值范围的中位数 1023。特别的,当 e 等于 0 且小数位均为 0 时,表示 0,当 e 全为 1 且小数位均为 0 时,表示无穷,如果 e 全为 1 且小数位不均为 0 ,那么这不是一个数(NaN)。
对于 0.1 来说,转成二进制小数为:
0.1.toString(2)
// "0.0001100110011001100110011001100110011001100110011001101"
用科学计数法表示:1.100110011001100110011001100110011001100110011001101 x 2^-4
,指数为 -4,因此 e 等于 1019,转成 11 位二进制数 01111111011
;尾数为 1001100110011001100110011001100110011001100110011010
,末尾补齐 0 至 52 位数;正负号标志 s 显然是 0。最终 0.1 在内存中长这样 0011111110111001100110011001100110011001100110011001100110011010
,图形化显示为:
图形化 0.1 的存储
浮点数的舍入
小数位其实是 1001
无限循环,根据 IEEE754 的舍入标准 进行了舍入,导致结果会比 0.1 大那么一点点,之所以使用的时候没感觉出来,是因为打印结果的时候舍入了小数点后第 18 位的值。Number
对象里有 toPrecision
方法来指定精度,我们可以来看一下:
0.1.toPrecistion(20);
// "0.10000000000000000555"
0.2.toPrecistion(20);
// "0.20000000000000001110"
0.3.toPrecistion(20);
// "0.29999999999999998890"
解决方法
知道了原因,就能对症下药找到解决的方法,这里我们要分情况讨论:
数据展示:
只需要展示的话,可以 toFixed
或者 toPrecision
选择自己需要的精度,然后再 parseFloat
转成浮点数。需要注意的是,这两个方法均不是四舍五入法,而是上面提过的 IEEE754舍入标准,舍入至最接近的值,如果有 2 个值一样接近,则取偶数值。
数据运算:
如需对数据进行运算,那么一种常用的方法就是,把小数转成整数再进行运算,只要运算过程涉及到的数字不大于 MAX_SAFE_INTEGER
,得到的结果就是“精确”的。
特殊的数字
Javascript 里定义了几个特殊的数字:Number.MAX_SAFE_INTEGER
、Number.MIN_SAFE_INTEGER
、Number.MAX_VALUE
、Number.MIN_VALUE
、Infinity
。分别是什么意思呢?
Number.MAX_SAFE_INTEGER
是最大安全整数,值为9007199254740991
,再大就会出现溢出,导致意外的结果,如:Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2
结果为true
。Number.MIN_SAFE_INTEGER
同理,值为-9007199254740991
。Number.MAX_VALUE
是最大能表示的值1.7976931348623157e+308
,等于2^1024 - 1
,加 1 即为Infinity
。Number.MIN_VALUE
是最小能表示的正值5e-324
,是最接近 0 的值。Infinity
表示无穷大,有正无穷和负无穷。
这些数的值都是有原因的,可以试着根据上面说的浮点数存储格式来推导这些数:
最大最小整数:
最大的整数会占满小数位部分,一共是 52 位,由于首位必定为 1 被省略了,所以最大整数共有 53 位数字,因此值为 2^53 - 1
,最小整数加个负号即可。
无穷大:
标准规定,指数位全为 1 且 小数位全为 0 表示无穷大,理论值为 2^1024
。
最大可表示的值:
无穷大减 1,即为 2^1024 - 1
。
最小可表示的正值:
标准规定,指数位全为 0 且 小数位也全为 0 表示 0,因此,最接近零的数就是指数位为 0,小数位最小位为 1,其他为 0,2^-1023 * 2^-51
,等于 2^-1074
。
Math.pow(2, -1074)
// 5e-324
更好的解决方法
不要重复造轮子,有现成的类库为啥不用?
传送门:Mathjs、bignumberjs