Javascript作为一门大型编程语言,在日常开发中难免会涉及到大量的数学计算。然而,浮点数在计算过程中可能出现精度的问题,因此Javascript提供了一个高精度计算库来帮助处理复杂的数字计算。本文就来介绍一下Javascript高精度计算及其相关知识。
首先,我们来看一个简单的例子:
js
0.1 + 0.2 //结果不是 0.3,而是 0.30000000000000004
可以看到数字的精度已经丢失,虽然结果相差无几,但是作为技术人员,这绝对不可以忽略。 简单一句话概括解释为什么你会得到意想不到的结果:
因为在计算机内部,使用的二进制浮点根本就不能准确地表示像 0.1, 0.2 或 0.3 这样的数字。
当编码或解释代码时,你的 "0.1" 其实已经舍入为和该数字的最接近的数字,即使在计算发生之前已经会导致小的舍入误差。
JavaScript 中的数字都是浮点数,即使看起来像整数的数字也是。这是因为 JavaScript 使用 IEEE 754 标准来表示数字,这种表示方法对于大多数情况是足够的,但在某些情况下可能导致精度丢失。
在涉及货币或其他需要精确计算的场景中,由于 JavaScript 浮点数的特性可能导致精度丢失,因此一种常见而有效的解决方案是将数字转换为整数进行计算,然后再将结果转换回浮点数。这种做法能够在一定程度上规避浮点数运算中可能出现的舍入误差,尤其在处理金融数据等对精确性要求极高的情况下显得尤为重要。
js
let num1 = 0.1 * 10; // 转换成整数进行计算
let num2 = 0.2 * 10;
let sum = (num1 + num2) / 10;
// 转换回浮点数
console.log(sum); // 输出:0.3
通过上面这种方式,我们可以在保留所需精度的同时,规避掉 JavaScript 浮点数运算可能引发的不精确性问题。
但是也会出现其他问题,增加小数点后面的位数,会出现下面的情况:
js
20.24*100
// 2023.9999999999998
我们知道浮点型数据类型主要有:单精度float、双精度double。
但是!!!
JavaScript 存储小数和其它语言如 Java 和 Python 都不同,JavaScript 中所有数字包括整数和小数都只有一种类型 即 Number类型 它的实现遵循 IEEE 754 标准,IEEE 754 标准的内容都有什么,这个咱不用管,我们只需要记住以下一点:
javascript以64位双精度浮点数存储所有Number类型值,即计算机最多存储64位二进制数。
对于double型数据(双精度浮点数),其长度是8个字节(大小),右边52位用来表示小数点后面的数字,中间11位表示e(exponent)小数点移动的位数,左边一位用来表示正负。如图所示:
解决方法
js
Number(parseFloat(20.24*100).toPrecision(16))
存储二进制时小数点的偏移量最大为52位,最多可表示的十进制为9007199254740992,对应科学计数尾数是 9.007199254740992,这也是 JavaScript 最多能表示的精度。它的长度是 16,所以可以使用 toPrecision(16) 来做精度运算。
通过先转为浮点型计算,然后做精度运算后再转为Number类型即可。
但是不能保证还会不会有其他问题,并且这样的计算太繁琐,每次都需要对数字进行相应的处理。
解决方案
我们将处理的计算问题进行统一封装,可以专门处理精度问题。代码如下:
ts
export class Calc{
/**
* 加法运算
* @param {number} num1
* @param {number} num2
* @returns {*}
*/
add(num1: number, num2: number): number {
num1 = Number(num1);
num2 = Number(num2);
let dec1: number, dec2: number, times: number;
try { dec1 = this.countDecimals(num1)+1; } catch (e) { dec1 = 0; }
try { dec2 = this.countDecimals(num2)+1; } catch (e) { dec2 = 0; }
times = Math.pow(10, Math.max(dec1, dec2));
const result = (this.mul(num1, times) + this.mul(num2, times)) / times;
return this.getCorrectResult("add", num1, num2, result);
}
/**
* 减法运算
* @param {number} num1
* @param {number} num2
* @returns {number}
*/
sub(num1: number, num2: number): number {
num1 = Number(num1);
num2 = Number(num2);
let dec1: number, dec2: number, times: number;
try { dec1 = this.countDecimals(num1)+1; } catch (e) { dec1 = 0; }
try { dec2 = this.countDecimals(num2)+1; } catch (e) { dec2 = 0; }
times = Math.pow(10, Math.max(dec1, dec2));
const result = Number((this.mul(num1, times) - this.mul(num2, times)) / times);
return this.getCorrectResult("sub", num1, num2, result);
}
/**
* 除法运算
* @param {number} num1
* @param {number} num2
* @returns {number}
*/
div(num1: number, num2: number): number {
num1 = Number(num1);
num2 = Number(num2);
let t1 = 0, t2 = 0, dec1: number, dec2: number;
try { t1 = this.countDecimals(num1); } catch (e) { }
try { t2 = this.countDecimals(num2); } catch (e) { }
dec1 = this.convertToInt(num1);
dec2 = this.convertToInt(num2);
const result = this.mul((dec1 / dec2), Math.pow(10, t2 - t1));
return this.getCorrectResult("div", num1, num2, result);
}
/**
* 乘法运算
* @param {number} num1
* @param {number} num2
* @returns {number}
*/
mul(num1: number, num2: number): number {
num1 = Number(num1);
num2 = Number(num2);
let times = 0, s1 = num1.toString(), s2 = num2.toString();
try { times += this.countDecimals(s1); } catch (e) { }
try { times += this.countDecimals(s2); } catch (e) { }
const result = this.convertToInt(s1) * this.convertToInt(s2) / Math.pow(10, times);
return this.getCorrectResult("mul", num1, num2, result);
}
/**
* 计算小数位的长度
* @param {*} num
* @returns {number}
*/
private countDecimals(num: any): number {
let len = 0;
try {
num = Number(num);
let str = num.toString().toUpperCase();
if (str.split('E').length === 2) { // 科学记数法
let isDecimal = false;
if (str.split('.').length === 2) {
str = str.split('.')[1];
if (parseInt(str.split('E')[0]) !== 0) {
isDecimal = true;
}
}
let x = str.split('E');
if (isDecimal) {
len = x[0].length;
}
len -= parseInt(x[1]);
} else if (str.split('.').length === 2) { // 十进制
if (parseInt(str.split('.')[1]) !== 0) {
len = str.split('.')[1].length;
}
}
} catch(e) {
throw e;
} finally {
if (isNaN(len) || len < 0) {
len = 0;
}
return len;
}
}
/**
* 将小数转成整数
* @param {*} num
* @returns {*}
*/
private convertToInt (num: any): number {
num = Number(num);
let newNum = num;
let times = this.countDecimals(num);
let temp_num = num.toString().toUpperCase();
if (temp_num.split('E').length === 2) {
newNum = Math.round(num * Math.pow(10, times));
} else {
newNum = Number(temp_num.replace(".", ""));
}
return newNum;
}
/**
* 确认我们的计算结果无误,以防万一
* @param {string} type
* @param {number} num1
* @param {number} num2
* @param {number} result
* @returns {number}
*/
private getCorrectResult(type: 'add' | 'sub' | 'div' | 'mul', num1: number, num2: number, result: number): number {
let temp_result = 0;
switch (type) {
case "add":
temp_result = num1 + num2;
break;
case "sub":
temp_result = num1 - num2;
break;
case "div":
temp_result = num1 / num2;
break;
case "mul":
temp_result = num1 * num2;
break;
}
if (Math.abs(result - temp_result) > 1) {
return temp_result;
}
return result;
}
}
希望这个方法能够帮助到遇到问题的小伙伴们。
总结
JavaScript 中的浮点数丢失精度问题是由底层表示方式引起的,因此在进行重要的精确计算时需要格外小心。选择合适的方法,如整数计算、使用专门的库或小数点后截断,可以帮助我们在实际应用中处理这些问题,确保得到精确的结果。在不同场景中选择适当的方法,是程序员需要谨慎考虑的问题,以避免潜在的错误。
最后,希望小伙伴们给我个免费的点赞