JavaScript计算出现精度丢失问题的解决方法
作者:hikits
前言
Javascript作为一门大型编程语言,在日常开发中难免会涉及到大量的数学计算。然而,浮点数在计算过程中可能出现精度的问题,因此Javascript提供了一个高精度计算库来帮助处理复杂的数字计算。本文就来介绍一下Javascript高精度计算及其相关知识。
首先,我们来看一个简单的例子:
0.1 + 0.2
结果不是 0.3,而是 0.30000000000000004
可以看到数字的精度已经丢失,虽然结果相差无几,但是作为技术人员,这绝对不可以忽略。 简单一句话概括解释为什么你会得到意想不到的结果:
因为在计算机内部,使用的二进制浮点根本就不能准确地表示像 0.1, 0.2 或 0.3 这样的数字。
当编码或解释代码时,你的 “0.1” 其实已经舍入为和该数字的最接近的数字,即使在计算发生之前已经会导致小的舍入误差。
JavaScript 中的数字都是浮点数,即使看起来像整数的数字也是。这是因为 JavaScript 使用 IEEE 754 标准来表示数字,这种表示方法对于大多数情况是足够的,但在某些情况下可能导致精度丢失。
在涉及货币或其他需要精确计算的场景中,由于 JavaScript 浮点数的特性可能导致精度丢失,因此一种常见而有效的解决方案是将数字转换为整数进行计算,然后再将结果转换回浮点数。这种做法能够在一定程度上规避浮点数运算中可能出现的舍入误差,尤其在处理金融数据等对精确性要求极高的情况下显得尤为重要。
let num1 = 0.1 * 10; // 转换成整数进行计算 let num2 = 0.2 * 10; let sum = (num1 + num2) / 10; // 转换回浮点数 console.log(sum); // 输出:0.3
通过上面这种方式,我们可以在保留所需精度的同时,规避掉 JavaScript 浮点数运算可能引发的不精确性问题。
但是也会出现其他问题,增加小数点后面的位数,会出现下面的情况:
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)小数点移动的位数,左边一位用来表示正负。如图所示:
解决方法
Number(parseFloat(20.24*100).toPrecision(16))
存储二进制时小数点的偏移量最大为52位,最多可表示的十进制为9007199254740992,对应科学计数尾数是 9.007199254740992,这也是 JavaScript 最多能表示的精度。它的长度是 16,所以可以使用 toPrecision(16) 来做精度运算。
通过先转为浮点型计算,然后做精度运算后再转为Number类型即可。
但是不能保证还会不会有其他问题,并且这样的计算太繁琐,每次都需要对数字进行相应的处理。
解决方案
我们将处理的计算问题进行统一封装,可以专门处理精度问题。代码如下:
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 中的浮点数丢失精度问题是由底层表示方式引起的,因此在进行重要的精确计算时需要格外小心。选择合适的方法,如整数计算、使用专门的库或小数点后截断,可以帮助我们在实际应用中处理这些问题,确保得到精确的结果。在不同场景中选择适当的方法,是程序员需要谨慎考虑的问题,以避免潜在的错误。
到此这篇关于JavaScript计算出现精度丢失问题的解决方法的文章就介绍到这了,更多相关JavaScript精度丢失内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!