【JavaScript】解决JS 计算精度问题,附完整的计算方案

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 中的浮点数丢失精度问题是由底层表示方式引起的,因此在进行重要的精确计算时需要格外小心。选择合适的方法,如整数计算、使用专门的库或小数点后截断,可以帮助我们在实际应用中处理这些问题,确保得到精确的结果。在不同场景中选择适当的方法,是程序员需要谨慎考虑的问题,以避免潜在的错误。

最后,希望小伙伴们给我个免费的点赞

相关推荐
小马哥编程1 小时前
Function.prototype和Object.prototype 的区别
javascript
王小王和他的小伙伴1 小时前
解决 vue3 中 echarts图表在el-dialog中显示问题
javascript·vue.js·echarts
学前端的小朱2 小时前
处理字体图标、js、html及其他资源
开发语言·javascript·webpack·html·打包工具
outstanding木槿2 小时前
react+antd的Table组件编辑单元格
前端·javascript·react.js·前端框架
好名字08212 小时前
前端取Content-Disposition中的filename字段与解码(vue)
前端·javascript·vue.js·前端框架
摇光932 小时前
js高阶-async与事件循环
开发语言·javascript·事件循环·宏任务·微任务
胡西风_foxww3 小时前
【ES6复习笔记】Class类(15)
javascript·笔记·es6·继承··class·静态成员
布兰妮甜3 小时前
使用 WebRTC 进行实时通信
javascript·webrtc·实时通信
艾斯特_3 小时前
JavaScript甘特图 dhtmlx-gantt
前端·javascript·甘特图
飞翔的渴望3 小时前
react18与react17有哪些区别
前端·javascript·react.js