一、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_VALUE和Number.MIN_VALUE来得到证实。同样可以通过Number.MIN_SAFE_INTEGER和Number.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
- 配置 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' }) - 链接 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)在链的值上 执行,返回值的字符串表示。- 扩展

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

被折叠的 条评论
为什么被折叠?



