Js小数点计算的精度问题解决

背景

主要是由于浮点数的表示方式以及计算机的二进制运算原理导致的JavaScript使用IEEE 754标准定义了浮点数的表示和计算规则。在这种表示方式中,浮点数由符号位、指数位和尾数位组成。尾数位的长度是固定的,一般为53位。但是,无论是整数还是小数,都需要通过二进制表示来存储,而不是十进制。这就导致了某些十进制小数在二进制表示中是无限循环的,因此无法完全精确地表示。

例如,0.1这个十进制小数在二进制中是无限循环的,无法准确表示。因此,在进行计算时,可能会出现舍入误差。多次的舍入误差累积起来就会导致精度缺失问题。

此外,JavaScript中的数值计算是通过二进制进行的,而不是十进制。由于二进制和十进制之间的转换存在精度限制,导致一些十进制的小数无法完全准确地转换为二进制,进而引发精度缺失。

为了解决这个问题,可以使用一些特殊的库或技术,如BigNumber.js或Decimal.js,来处理大数和小数运算,以提高精度和准确性。另外,在涉及到金额计算等需要高精度的场景中,可以将数值转换为整数进行计算,然后再进行必要的除法或格式化操作,以减少精度缺失的影响。

上面都是百度出来的, 两个插件库 decimal.js & Bignumber.js , 有兴趣的可以看这个文章 https://blog.csdn.net/weixin_44582045/article/details/131377847

个人认为前端项目中很少遇到很大的数值的计算, 一般是小数值的计算, 根据上面所说的概念很容易产生精度问题, 提供一个 化整格式化 的方法, 供参考
如果很明确小数点位数,代码如下

// 通过正则的方式, 将小数点后移, 模拟 数值计算的一个过程
// decimal 需要传 两个计算的数值 最大的小数点的位数
const getFixed = (num,decimal)=>{
    let regex = new RegExp(`\\.([\\d]{${decimal}})`)
    let format = num.toString().padEnd(decimal + num.toString().length,'0')
    return  format.replace(regex,'$1.') * 1   
}

let num1 = 0.11
let num2 = 0.22

// 加
let total = Number((getFixed(num1,2) + getFixed(num2,2))) / 100 

// 减
let subtrac = Number((getFixed(num1,2) - getFixed(num2,2))) / 100 

// 乘
let mult = Number((getFixed(num1,2) * getFixed(num2,2))) / 10000 

// 除的最终结果不用处理, 两个值 *100 算出来的 其实跟 不 * 的结果是一样的
let division = Number((getFixed(num1) / getFixed(num2)))


两个小数不确定小数点位数, 并且想动态计算乘除

// new Function 也可以 用 eval() 替代, 写法上稍微有点不同
// 例如:
// new Function(`return ${acc}${this.computeSign}${this.computeFn(curr)}`)()
// 可以修改为, 没有 return 也 不用调用
// eval(`${acc}${this.computeSign}${this.computeFn(curr)}`)
// 但是 new Function 在浏览器安全方面, 比eval 更优

class FormulaFn {
    constructor(){
        this.type = null
        this.numbers = null
        this.computeSign = null
        this.maxDecimalLength = 0
    }
    init(...arg){
        // v2中这样用
        // let numbers = [].slice.call(arguments, 0, -1)
        // v3中这样用
        let [type, ...numbers] = arg
        // 加减乘除
        let computeSign = {
            1: `+`,
            2: `-`,
            3: `*`,
            4: `/`,
        }[type]
        this.type = type
        this.numbers = numbers
        this.computeSign = computeSign
        this.maxDecimalLength = 0
        this.maxDecimalLengthFn()
    }
    // 判断一组数据, 取最大的小数点数量
    maxDecimalLengthFn() {
        let maxDecimalLength = this.numbers.reduce(
            (max, num) => {
                const decimalLength = (num.toString().split('.')[1] || '').length;
                return Math.max(max, decimalLength);
        }, 0);

        this.maxDecimalLength = maxDecimalLength
    }

    // 根据小数点点数 组装一个数据,用于最终计算回正确数据
    formulaStrFn() {
        let formula = [
            [ item=>[1,2].includes(item), () => new Function(`return ('1'.padEnd(1+${this.maxDecimalLength}, '0'))`)()],
            [ item=>[3].includes(item), () => new Function(`return ('1'.padEnd(1+${(this.maxDecimalLength * this.numbers.length)}, '0'))`)()],
            [ item=>[4].includes(item), () => 1]

        ]
        let formulaStr = formula.find((item) => item[0](this.type))?.[1]() || '1'
        return formulaStr
    }
    // 组装自身数据格式
    computeFn(curr){
        let format = curr.toString().padEnd(this.maxDecimalLength + curr.toString().length,'0')
        let regex = new RegExp(`\\.([\\d]{${this.maxDecimalLength}})`)
        return  format.replace(regex,'$1.') * 1   
    }

    getTotalNum(...arg){
    
        this.init(...arg)
        let baseNum = [3,4].includes(this.type) ? 1 : 0
        let totalNum = this.numbers.reduce((acc,curr)=>{
            return new Function(`return ${acc}${this.computeSign}${this.computeFn(curr)}`)()
        },baseNum)
        return totalNum /  this.formulaStrFn()
    }


}

export default FormulaFn


如何使用呢

import formulaFn from 'formulaFn.js'
let formulaFnClass = new formulaFn()

formulaFnClass.getTotalNum(0.1,0.22, 4)

拓展



// new Decimal(a).add(new Decimal(b)) 

// new BigNumber(0.7).plus(x).plus(y).toString())

// 这两种调用方式和上面的调用方式一样, 都可以用 class 类 的方式实现, Decimal 的方式就不赘述了
// class 类中 用递归的方式就可以实现

// 着重研究下 BigNumber 的实现方式, 写一个简单的 demo

class BigNumber {
    constructor(value) {
        this.value = Number(value);
    }
    
    add(num) {
        this.value = (this.value + Number(num));
        return this; // 返回当前实例以支持链式调用
    }
    // 输出结果
    toString() {
        return this.value; // 保留一位小数
    }
}

// 示例用法:用一个 class 重复调用, this.value 每次都是计算后的最新值, 然后最后输出
// 这种场景其实很好玩,能拓展很多方法出来, 学习, 学习!
// 这种方式比我上面的方法各有千秋吧, 不想在写一种方法了, 有兴趣的自己研究吧 !!!
const result = new BigNumber(0.7).add(0.1).add(0.1);
result.toString() // 输出 '0.9'




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值