最近在做项目的时候,出现了js计算时精度丢失的问题:
- 通过计算两个数相加来判断结果是否相等(0.1+0.2===0.3 // false);
- 涉及到金额的计算(保留小数点后两位)。刚开始的时候将金额的精度到分来进行计算;即:金额*100(17.56 * 100 = 1755.9999999999998)
浮点数运算后的精度问题
// 加法 =====================
0.1 + 0.2 = 0.30000000000000004
0.7 + 0.1 = 0.7999999999999999
0.2 + 0.4 = 0.6000000000000001
// 减法 =====================
1.5 - 1.2 = 0.30000000000000004
0.3 - 0.2 = 0.09999999999999998
// 乘法 =====================
17.56 * 100 = 1755.9999999999998
0.8 * 3 = 2.4000000000000004
35.41 * 100 = 3540.9999999999995
// 除法 =====================
0.3 / 0.1 = 2.9999999999999996
0.69 / 10 = 0.06899999999999999
从上面可以看到,0.1+0.2!==0.3并且0.1+0.2的结果是0.30000000000000004。为什么会出现这样的结果呢?
我们一块来探索一下这背后的原因
十进制和二进制表示的小数特点:
-
日常生活中人类使用的是十进制,如果使用以10为底的质因数,则可以精确表示分数。
- 2和5是10的质因数。
- 1/2、1/4、1/5 (0.2)、1/8 和 1/10 (0.1) 可以精确表示,因为分母使用 10 的质因数。
- 而 1/3、1/6 和 1/7 是重复小数,因为分母使用 3 或 7 的质因数。
-
另一方面,在计算机使用的是二进制,如果使用以 2为底的素因子,则可以精确表示分数。
- 2 是 2 的唯一质因数。
- 所以 1/2、1/4、1/8 都可以精确表示,因为分母使用 2 的质因数。
- 而 1/5 (0.2) 或 1/10 (0.1) 是重复小数。
我们在计算数学问题的时候,使用的是十进制,计算0.1 + 0.2的结果等于0.3,没有任何问题。但在计算机中,存储数据使用的是二进制,数据由0和1组成。所以在对数据进行计算时,需要将数据全部转换成二进制,再进行数据计算。
那么JavaScript在计算0.1+0.2时到底发生了什么呢?首先,十进制的0.1和0.2会被转换成二进制的,但是由于浮点数用二进制表示时是无穷的:
0.1 -> 0.0001 1001 1001 1001...(1100循环)
0.2 -> 0.0011 0011 0011 0011...(0011循环)
IEEE 754 标准的 64 位双精度浮点数的小数部分最多支持53位二进制位,所以两者相加之后得到二进制为:
0.00011001100110011001100110011001100110011001100110011010
+0.00110011001100110011001100110011001100110011001100110100
------------------------------------------------------------
=0.01001100110011001100110011001100110011001100110011001110
因浮点数小数位的限制而截断的二进制数字,再转换为十进制,就成了0.30000000000000004。所以js在进行计算时会出现精度丢失问题,从而产生误差。
解决方法:
1、确定精度的情况下--项目中的解决方法:
function numbersequal(number){
return Math.round(number * 100)
}
console.log(numbersequal(17.56)); // 1756
console.log(numbersequal(0.1756)); // 18
缺点:精度不确定的情况下会依然会出现缺失的问题
2、转换为整数计算
function add(num1, num2) {
//num1 小数位的长度
const num1Digits = (num1.toString().split('.')[1] || '').length;
//num2 小数位的长度
const num2Digits = (num2.toString().split('.')[1] || '').length;
// 取最大的小数位作为10的指数
const baseNum = Math.pow(10, Math.max(num1Digits, num2Digits));
// 把小数都转为整数然后再计算
return (num1 * baseNum + num2 * baseNum) / baseNum;
}
console.log(add(0.1 + 0.2)) // 0.3
复制代码
把计算数字 提升 10 的N次方 倍 再 除以 10的N次方。N>1。
3、将小数位转换成字符串去除 . 后进行相乘计算
function mathMultiply(arg1, arg2) {
let m = 0;
let s1 = arg1.toString();
let s2 = arg2.toString();
try {
m += s1.split('.')[1].length; // 小数相乘,小数点后个数相加
} catch (e) {}
try {
m += s2.split('.')[1].length;
} catch (e) {}
return (
(Number(s1.replace('.', '')) * Number(s2.replace('.', ''))) /
Math.pow(10, m)
);
}
> mathMultiply(0.1, 0.2); // 0.02
> mathMultiply(1, 2); // 2
复制代码
2、3的缺点:
- JS中的存储都是通过8字节的double浮点类型表示的,因此它并不能准确记录所有数字,它存在一个数值范围
Number.MAX_SAFE_INTEGER为 9007199254740991,而Number.MIN_SAFE_INTEGER为 -9007199254740991,超出这个范围的话JS是无法表示的 虽然范围有限制,但是数值一般都够用
4、使用ES6提供的极小数Number.EPSILON:
Number.EPSILON
// 2.220446049250313e-16
Number.EPSILON.toFixed(20)
// "0.00000000000000022204"
复制代码
引入一个极小的量,目的在于为浮点数计算设置一个误差范围,如果误差能够小于Number.EPSILON,我们就可以认为结果是可靠的。
误差检查函数(出自《ES6标准入门》-阮一峰)
function withinErrorMargin (left, right) {
return Math.abs(left - right) < Number.EPSILON
}
console.log(withinErrorMargin(0.1+0.2,0.3)); //true
5、类库:
(1)、 Math.js
介绍:功能强大,内置大量函数,体积较大
Github:github.com/josdejong/m…
(2)、decimal.js
为 JavaScript 提供十进制类型的任意精度数值。
GitHub:github.com/MikeMcl/dec…
- 加法
Decimal.add(1, 2); // 3
Decimal.add(0.1, 0.2) // 0.3
const x = new Decimal(1);
const result = x.plus(2); // 3
复制代码
- 减法
Decimal.sub(3, 1); // 2
Decimal.sub(0.3, 0.1) // 0.2
const x = new Decimal(3);
const result = x.sub(1); // 2
复制代码
- 乘法
Decimal.mul(3, 2); // 6
Decimal.mul(17.56, 100) // 1756
const x = new Decimal(3);
const result = x.mul(2); // 6
复制代码
- 除法
Decimal.div(6, 2); // 3
Decimal.div(17.56, 100) // 0.1756
Decimal.div(17.56, 0) // Infinity
const x = new Decimal(6);
const result = x.div(2); // 3
复制代码
使用除法时候要注意 除数(分母)不能为0。
总结:
在js计算的过程中会出现精度丢失的问题,通过分析发现原因:十进制转二进制时,因浮点数小数位的限制而截断的二进制数字,再转换为十进制,从而产生误差。
注:站在前人的肩膀上,可以前进的更快、更远。当项目中出现大量的js加减乘除计算的时候,建议使用成熟的库,虽然部分函数可能永远不会用到,但是封装的方法还是值得相信的(●'◡'●)。