Javascript计算精度问题解决方案
浮点值的精确度最高可达17位小数,但在算术计算中远不如整数精确。之所以存在这种舍入错误,是因为使用了IEEE 754数值,这种错误并非ECMAScript所独有,其它使用相同格式的语言也有这个问题。只不过在很多其他语言中已经封装好了方法来避免精度的问题,而 JavaScript 是一门弱类型的语言,从设计思想上就没有对浮点数有个严格的数据类型,所以精度误差的问题就显得格外突出。
精度常见问题
场景一:浮点运算
// 加法
0.1 + 0.2 = 0.30000000000000004
// 减法
0.2 - 0.11 = 0.09000000000000001
// 乘法
0.2 * 0.1 = 0.020000000000000004
// 除法
0.3 / 0.2 = 1.4999999999999998场景二:四舍五入系列方法
一般用于金额计算或格式化小数。
1.05.toFixed(1) // 1.1
1.005.toFixed(2) // 1.00
1.0005.toFixed(3) // 1.000
1.00005.toFixed(4) // 1.0001
Math.round(244.5) // 245
Math.round(2.445 * 100) //244 场景三:转换函数
parseInt(0.29 * 100, 10) // 28以上场景都是浮点数的精度问题:带有计算的精度丢失问题,是由于在计算中有从十进制转换为二进制后无限循环的小数,而计算机的内存限制不能存储这样的值,只能存储一个近似值,所以当计算机存储后再取出来时就会出现精度问题;toFixed的精度丢失与转换后的数值存储及四舍五入规则有关。
场景四:整数精度
19571992547450991===19571992547450992 // true
Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2 // true同样的原因,由于内存限制,Javascript不能存储所有的整数,精确整数的范围是 -(2^53-1) ~ 2^53-1,即-9007199254740991 ~ 9007199254740991。
精度问题解决方案
简单运算
如果只是想解决简单的小数计算问题,可以将浮点数转换成整数进行计算,然后再将整数的小数点位调整,转回正确的浮点数结果。
// 获取小数位数
function getDecimalCount(num) {
let count;
try {
count = num.toString().split('.')[1].length;
} catch(e) {
count = 0;
}
return count;
}
// 判断是否位数值
function isNum(num) {
return "number" === typeof num;
}
// 加法 - 修复精度
function decimalAdd(num1, num2) {
if (!isNum(num1) || !isNum(num2)) return;
const count1 = getDecimalCount(num1);
const count2 = getDecimalCount(num2);
const max = Math.pow(10, Math.max(count1, count2));
if (count1 === count2) {
num1 = (num1 + '').replace('.', '');
num2 = (num2 + '').replace('.', '');
} else if (count1 > count2) {
const fillZero = '0'.repeat(count1 - count2);
num1 = (num1 + '').replace('.', '');
num2 = (num2 + '').replace('.', '') + fillZero;
} else {
const fillZero = '0'.repeat(count2 - count1);
num1 = (num1 + '').replace('.', '') + fillZero;
num2 = (num2 + '').replace('.', '');
}
return (Number(num1) + Number(num2)) / max;
}
// 减法-修复精度
// 减法转成加法计算
function decimalSub(num1, num2) {
if (!isNum(num1) || !isNum(num2)) return;
return decimalAdd(num1, -num2);
}
// 乘法-修复精度
function decimalMul(num1, num2) {
if (!isNum(num1) || !isNum(num2)) return;
const sum = getDecimalCount(num1) + getDecimalCount(num2);
num1 = (num1 + '').replace('.', '');
num2 = (num2 + '').replace('.', '');
return Number(num1) * Number(num2) / Math.pow(10, sum);
}
// 除法 - 修复精度
function decimalDiv(num1, num2) {
if (!isNum(num1) || !isNum(num2)) return;
const count1 = getDecimalCount(num1);
const count2 = getDecimalCount(num2);
const formatNum1 = Number((num1 + '').replace('.', ''));
const formatNum2 = Number((num2 + '').replace('.', ''));
if (count2 - count1 < 0) {
// 由于 Math.pow(10, count2 - count1) 为小数,需要处理精度问题
return decimalMul((formatNum1 / formatNum2), Math.pow(10, count2 - count1));
} else {
return (formatNum1 / formatNum2) * Math.pow(10, count2 - count1);
}
}测试结果:
console.log(1.1 + 1.3) // 2.4000000000000004
console.log(decimalAdd(1.1, 1.3)) // 2.4
console.log(1.3 - 1.1) // 0.19999999999999996
console.log(decimalSub(1.3, 1.1)) // 0.2
console.log(19.9 * 100) // 1989.9999999999998
console.log(decimalMul(19.9, 100)) // 1990
console.log(19.9 / 10) // 1.9899999999999998
console.log(decimalDiv(19.9, 10)) // 1.99复杂运算
通常这种对精度要求高的计算都应该交给后端去计算和存储,因为后端有成熟的库来解决这种计算问题。不过目前前端也已经有很多成熟的库来解决JS数值计算精度的问题。
math.js
math.js是一个用于JavaScript和Node.js的扩展数学库,支持符号计算,附带了大量内置函数和常量,并提供了一个集成的解决方案来处理不同的数据类型,如数字、大数、复数、分数、单位和矩阵。与JavaScript内置数学库兼容。
https://mathjs.org
decimal.js
decimal.js 实现了对任意精度的十进制进行计算。
https://mikemcl.github.io/decimal.js/
结语
这篇文章主要是梳理Javascript中常见的计算精度问题及解决方案,希望能帮助到大家~