【JS】详解JS精度丢失原理以及业务中价格计算引入数学库 Math.js的使用场景

本文深入探讨了JavaScript中Number类型导致的精度丢失问题,包括其内存存储原理、常见计算问题及背后的原因。同时介绍了math.js库的应用,以及如何通过BigNumber实现高精度计算,最后给出了业务实践案例。

一、JS为什么会出现精度丢失问题

    1、JS基本数据类型 Number 在内存中是怎么存储的?

        JS中的Number类型使用的是双精度浮点型,也就是其他语言中的double类型。在计算机内存中,单精度数是用32个 bit 来存储的浮点数,双精度数是使用64个 bit 来存储的浮点数。其中有1位符号位 (+/-),11位表示指数位(次方数),52位表示数值位(精确度) 内存结构如下:

        在ES规范中规定e的范围在-1074 ~ 971,而m能表示的最大数是52个1,最小能表示的是1 。计算机中存储小数是先转换成二进制进行存储的,二进制的第一位有效数字必定是1,因此这个1不会被存储,可以节省一个存储位,因此尾数部分可以存储的范围是1 ~ 2^(52+1),也就是说Number能表示的最大数字绝对值范围是 2^-1074 ~ 2^(53+971) ->5e-324~1.7976931348623157e+308,可以通过Number.MAX_VALUENumber.MIN_VALUE来得到证实。同样可以通过Number.MIN_SAFE_INTEGERNumber.MAX_SAFE_INTEGER 证实安全整数的范围。

    2、三个存在的问题

         1、在四则运算中存在精度丢失的问题,比如: 0.1 + 0.2       //0.30000000000000004

        精度缺失不是小于或者大于某个值就会出现精度缺失,而是落在固定间隔长度的数值中就会精度缺失,精度缺失的原因是该数值在内存中不能完全记录下来。

        前面提到,计算机中存储小数是先转换成二进制进行存储的,我们来看一下0.1和0.2转换成二进制的结果:

        十进制(0.1) =>二进制 (00011001100110011001(1001)...)

        十进制(0.2) => 二进制(00110011001100110011(0011)...)

        可以发现,0.1和0.2转成二进制之后都是一个无限循环的数,前面提到尾数位只能存储最多53位有效数字,这时候就必须来进行四舍五入了,最终的这个二进制数转成十进制就是0.30000000000000004。

        至此,这个精度丢失的问题已经解释清楚了,用一句话来概括就是,计算机中用二进制来存储小数,而大部分小数转成二进制之后都是无限循环的值,因此存在取舍问题,也就是精度丢失。

    2、超过最大安全整数的运算是不安全的,比如:9007199254740991 + 2        //9007199254740992

        最大安全整数9007199254740991对应的二进制数如图:

        53位有效数字都存储满了之后,想要表示更大的数字,就只能往指数数加一位,这时候尾数因为没有多余的存储空间,因此只能补0。 可以看出在指数位为53的情况下,最后一位尾数位为0的数字可以被精确表示,而最后一位尾数为为1的数字都不能被精确表示。也就是可以被精确表示和不能被精确表示的比例是1:1

        小结:之所以会有最大安全整数这个概念,本质上还是因为数字类型在计算机中的存储结构。在尾数位不够补零之后,只要是多余的尾数为1所对应的整数都不能被精确表示。 

        可以发现,不管是浮点数计算的计算结果错误和大整数的计算结果错误,最终都可以归结到JS的精度只有53位(尾数只能存储53位的有效数字)。那么我们在日常工作中碰到这两个问题该如何解决呢? 

        3、JS的 toFixed() 方法错误问题

        JS的 toFixed() 四舍五入规则与数学中的规则不同,而是采用银行家算法,俗称『四舍六入五成双』。对于位数很多的近似数,当有效位数确定后,其后面多余的数字应该舍去,只保留有效数字最末一位。

        『四』:≤4 时舍去

        『六』:≥6时进1

        『五』:根据5后面的数字来定,当5后有数字时舍5进1;当5后无数字时分两种情况,①5前为奇数,舍5入1;②5前为偶数,舍5不进。

二、精度丢失问题在业务里的体现和隐患

        1、精度丢失在具体计算的体现:

// 加法 =====================
0.1 + 0.2 = 0.30000000000000004
0.7 + 0.1 = 0.7999999999999999
0.2 + 0.4 = 0.6000000000000001

// 减法 =====================
1.5 - 1.2 = 0.30000000000000004
0.3 - 0.2 = 0.09999999999999998
 
// 乘法 =====================
19.9 * 100 = 1989.9999999999998
0.8 * 3 = 2.4000000000000004
35.41 * 100 = 3540.9999999999995

// 除法 =====================
0.3 / 0.1 = 2.9999999999999996
0.69 / 10 = 0.06899999999999999

        2、超过最大安全整数的运算或传参等场景

      整数会出现误差,小数超出长度会被丢弃。(如绿谷后管入组号为number类型长度超出js安全数字,因不涉及到计算,所以改为string类型解决详情跳转异常)

var numLen16 = '999999999666666'
numLen16.length // 16  长度16位 正整数
+numLen16 // '999999999666666' 没有误差

var numLen17 = '9999999999555557'
numLen17.length // 17  长度17位 正整数
+numLen16 // '9999999999555556' 从第17位开始出现误差

//json 化时的问题
var json = JSON.stringify({a:999999999955555777,b:true})
json // "{"a":999999999955555800,"b":true}"  也会出现误差

//小数超过18位左右,小数部分超过位数会被丢掉
9555555555555.34243535  // 9555555555555.342

        3、四舍五入算法问题

let num = 1.234
num.toFixed(2) //1.23 正确
 
let num = 1.235
num.toFixed(2) //1.24 正确
 
let num = 1.236
num.toFixed(2) //1.24 正确
 
let num = 0.234
num.toFixed(2) //0.23 正确
 
let num = 0.235
num.toFixed(2) //0.23 错误 X
 
let num = 0.236
num.toFixed(2) //0.24 正确

三、Math.js

  1. 配置   math.js | an extensive math library for JavaScript and Node.js
            Math.js 包含许多配置选项。这些选项可以应用于创建的 mathjs 实例并在之后更改。
    import { create, all } from 'mathjs'
    
    // create a mathjs instance with configuration
    const config = {
      epsilon: 1e-12,
      matrix: 'Matrix',
      number: 'number',
      precision: 64,
      predictable: false,
      randomSeed: null
    }
    const math = create(all, config)
    
    // read the applied configuration
    console.log(math.config())
    
    // change the configuration
    math.config({
      number: 'BigNumber'
    })
  2. 链接   math.js | an extensive math library for JavaScript and Node.js

    可以使用函数math.chain(value) 创建链。
    math.chain(3)
        .add(4)
        .subtract(2)
        .done() // 5
    
    math.chain( [[1, 2], [3, 4]] )
        .subset(math.index(0, 0), 8)
        .multiply(3)
        .done() // [[24, 6], [9, 12]]

    done() 完成链并返回链的值。
    valueOf() 与 相同done(),返回链的值。
    toString()math.format(value)在链的值上 执行,返回值的字符串表示。
    1. 扩展
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值