前言
在开发中,要进行计算,你可能会遇到小数运算,运气好的话,你的测试测不到精度问题,但其实这是很严重的,以下两个典型例子先感受以下
0.1 + 0.2 = 0.30000000000000004
35.41 * 100 = 3540.9999999999995
是不是出乎你的意料?
写这篇文章的原因是网上找了些资料,要不就是介绍不全的,要不就是存在错误的(可能大家没发现),要不就是方案还有待加强的。于是我决定自己整理出一份较为全面而不误导别人的文章出来(文章方案对网上大部分资料存在的缺陷进行弥补增强),如果您发现不足,请告诉我,虚心请教。
以下我们了解原因以及寻找解决方案。
原因
js的数字存储情况
在计算机中存储的信息都是二进制来表示,我们都知道,js中数字类型只有Number
,不像其他语言如java
有int
、double
类型等。它的实现遵循 IEEE 754 标准,使用64位固定长度来表示,也就是标准的 double 双精度浮点数。
其中这64位数又分为三部分(从左往右看):
- 符号位:第一位为符号位,0表示正数,1表示负数;
- 指数位:中间的11位存储指数,用来表示次方数
- 尾数位:最后的52位就是尾数,超出部分会0舍1入(类似四舍五入)
计算过程
以例子0.1 + 0.3
展开说明:
- 先把0.1和0.3转化位二进制,会发现转化后的二进制会陷入循环,超出了上面说的尾数52位,因为小数位只能到52位,所以进行0舍1入的处理,
0.1
->0.0001100110011001100110011001100110011001100110011010
;0.2
->0.0011001100110011001100110011001100110011001100110011
- 转化后的二进制进行加法运算,得
0.0100110011001100110011001100110011001100110011001101
,转化为十进制就是0.30000000000000004
于是就这样,得到了一个出乎你预料的结果了。并不是所有小数计算都会这样,是一些小数转化位二进制时出现超出52位数时,就可能会出现这种意外结果。
解决方案
上面我们知道了出现意外结果是因为小数转化为二进制时发生问题,那整数呢?很自然,整数也是有最大值安全值的,就是2的53次方,为9007199254740992。超出这个数值的计算同样也是带来精度问题。但是我们一般使用是不会超出这个值的(如果你的需求也要考虑超出这个数值,抱歉,我无能为力)
如果你的需求可以无视上面整数最大安全值的弊端,那么接下来的解决方案才是适合你的。
解放方案:把小数运算中的小数,升级转化为整数(乘以10的n次幂),在进行运算,将最后结果再降级(除以10的n次幂)
上面的描述仅仅是一个思路,一个转化思路。怎么将小数转化为整数,这就讲究了,不能真的进行乘法计算,乘以10的n次幂,还记得前言里的第二个例子吗,这种转化整数的方式本身就是一个小数运算,所以还是会出现问题。
我们要用字符串替换的方式来实现这个“升级”:
原值:1.23
转化过程:
1. 化为字符串 '1.23'
2. '1.23'.replace('.', ''),得'123',相当于乘以10的2次幂
结合实际例子来了解大概的一个情况,例子1.1 + 1.22
1. 分别对1.1和1.22进行字符串替换,变成'110'和'122',相当于乘以10的2次幂
2. 对替换结果转化回数字类型然后再进行加法运算:110 + 122 = 232
3. 对232除以10的2次幂,得2.32
以上就是一个转化和运算过程。
基于上述基本思想,我对加减乘除进行一个方法封装,方便大家进行小数运算。
/**
* 带有小数的加法/减法运算
* 减法实际上可看成加法,所以如果要做减法,只需第二个参数即被减数传负值即可
* @param {Number} arg1 - 加数/减数
* @param {Number} arg2 - 加数/被减数
*/
function addFloat(arg1, arg2) {
let m = 0; // 记录两个加数中最长的小数位长度
let arg1Str = arg1 + '';
let a