likes
comments
collection
share

浅谈一下 js 的 Number 、 小数与精度问题

作者站长头像
站长
· 阅读数 17

前言

js 中 Number 也是一个比较杂的数字,相比较 c语言 中的 整数、小数 分开,甚至长整型都要分开来说,js 中的 Number 代表的就比较多了,其可以表示整数、小数、大整数

其中整数是用着最舒服的,小数大整数都会存在精度问题

number

Number 中的安全数组 Number.MAX_SAFE_INTEGER9007199254740991 也就是 2^53 -1, 负数 9007199254740991,具体为何不是假加一,可能不是用的补码吧(瞎猜的😂), 代表着其最多有 52位整数给整数部分,还有一位是符号位,也就是说,小数部分11` 位(具体还是看其内部设定)

如果超出这个数字就不能显示了么,但会发现,它也能显示,应该见到过 number 有时候出现的科学计数法吧,这就是 Number 中的大数字,即:符号位 + 指数位 + 指数偏移 + 小数。也就意味着,`科学计数法用到了小数,小数存在的问题,它自然也存在

小数的精度是怎么丢失的(了解十进制怎么转化小数的)

前面总是提到小数丢失精度,那么是怎么丢失的呢,在此之前我们看先二进制小数怎么来的

众所周知十进制的表示方法, 十进制:21.1 = 2 * 10^1 + 1 * 10^0 + 1 * 10^(-1) = 10 + 1 + 0.1

那么二进制的表示方法类推,11.1(0b11.1) = 1 * 2^1 + 1 * 2^0 + 1 * 2^(-1) = 2 + 1 + 0.5 = 3.5

但是使用上面的方法,将二进制转化十进制还行,十进制转化二进制,那么逆推,和十进制一样,小数部分乘上进制取整可以依次得出对应位置的数字,这就是常见的 乘2取整法,一种比较常见的手段

我们先列举一个简单的小数 0.75

const v = 0.75
//可以看出,为 2^-1 + 2^-2 = 0.5 + 0.25 = 0.75
-	1.5(0.5)	1(0)
-	1		1
0b0.11

再来一个 0.29 吧,发现就两位小数,竟然出现了那么长的小数位数,并且咱们还没算完,会发现还能算很多位

const v = 0.29
//x2取整法,这是一比较常见的手段
- 0.58(0.58) 1.16(0.16) 0.32(0.32) 0.64(0.64) 1.28(0.28) 0.56(0.56) 1.12(0.12)
- 0          1          0          0          1          0          1
0b0.0100101

了解了二进制一小数的来源,会发现他和我们的小数看起来整不整关系似乎不是很大,当他们进行加减乘除的时候,由于小数位数有限,很可能出现计算的小数,实际比我们要求的十进制的数字略大或者略小,当然有时候是没问题的

因此也就了解了小数出现精度的原因,那么可以看出这个不仅仅是 js 的问题,其他语言也会存在这个问题

ps:为了避免小数出现精度问题,则引出了一系列解决方案,下面只介绍一部分

小数精度问题

为了避免小数的精度很问题,很多小白、大佬们也都得想办法,办法自然也出现了很多种,下面就简单列出一些手段

  • 对于精度要求很高,可能存在长整数、长小数的,直接跳出原生态语言的计算,写一套自己的计算方式(例如:使用几个字符串或者链表进行计算等,一些四则运算的算法可能就是基于此类需求衍生出来的吧)
  • 使用三方库,和上面类似,例如:decimal.js 库,不管精度要求高不高直接一套解决
  • 对于精度不高的,且小数尾部精度也不高的(一般都几位小数),直接四舍五入即可(一些算法可能会需要转化为整数在四舍五入)
  • 传递此类数值的时候使用字符串,避免传递的时候丢失精度

我们举一个最常见的例子,也是最经典的一个例子

看了是不是脑壳疼😂
0.2 + 0.1 = 0.30000000000000004

一些人解决他,觉得直接乘以10的n次幂让其正好变成整数在计算,不就行了么

乍一看解决了,很兴奋用上了
0.2 * 10 + 0.1 * 10 = 0.3

然后问题又来了,又来了这么一组数,前面的案例 0.29

//直接加发现有问题
0.28 + 0.01 = 0.29 = 0.29000000000000004
//直接 *100 给其转化为整数,发现好像又不行
0.28 * 100 + 0.01 * 100 = 29.000000000000004
//有人说,我在多增加一次幂这个案例就行了呀, *1000
0.28 * 1000 + 0.01 * 1000 = 290
//我想说你再加次幂试试,*10000,会感觉它还是不靠谱,都是巧合😂
0.28 * 10000 + 0.01 * 10000 = 2900.0000000000005

实际了解了前面的小数怎么来的,就应该想到,单纯用10禁止乘法有点不科学哈,再说乘的数字太大了,要是结果变成大整数,又变成了大整数的精度问题了

那怎么办呢,个人简单总结一下小技巧,省事就完了

对于精度要求非常高的场景,数字无论是整数还是小数都可能存在比较大、比较小的一位,或者不确定,担心出问题,直接上三方 decimal.js 之类的计算库计算就行了,虽然性能低一些,但是安全要紧

对于精度基本没啥要求,就是了解一下的,直接小数计算都没问题,前端显示的时候,直接 toFixed 就完事了,误差不会太大(类似四舍五入)

对于精度适中(例如:我们的百分比计算),无论是整数还是小数都不算大,直接使用 number 就可以表示的,直接转化为整数,或者转为精度不会太高的小数(别搞出十几位小数就行),计算完毕后,直接四舍五入就完事了

为什么上面四舍五入就可以解决精度问题,前面也了解了,小数出现精度问题,基本上都是极为靠后的精度,基本上影响不大,无论数值精度的偏差相对整体偏小、偏大,都会出现...9999x、...00000x的情况,由于位数比较靠后,因此对于前面的数字进行四舍五入不会有什么影响

Math.round, js 中四舍五入的方法,这个是四舍五入为整数的,会抹掉小数部分,有需要的可以先乘后除,这样精度就没问题

const num = 0.28 + 0.01
//先乘,四舍五入后,再除就没问题了
Math.round(num * 100) / 100 = 0.29

ps:需要注意的是对结果四舍五入,不是计算过程中的数字,毕竟二进制储存,只要计算仍然会存在问题,有保存显示需求,计算完毕最好转为字符串保存或者显示,否则可能传递过程中丢失精度也不是没可能

不得不提,js 中数字的 toFixed 函数四舍五入,这也是前端比较常用的一个方法,其和我们想象的可能有时不一样,其似乎是小于等于5 舍大于5入,其会参考后面的实际保存的值(有实际四舍五入需要可以使用上面的 Math.round解决)

const v  = 0.1028 + 0.052199999999
const v1  = 0.1029 + 0.0521
const v2 = 0.1028 + 0.052200000001
console.log(v, v1,  v2);
console.log(v.toFixed(2), v1.toFixed(2), v2.toFixed(2));

//打印结果
0.154999999999 0.155 0.155000000001
0.15 0.15 0.16

最后

如果出现了计算中小数精度的问题,这篇文章看完,基本上就能明白什么情况了,还能从中找到一些解决方案,甚至避免一些实际操作中的失误

转载自:https://juejin.cn/post/7384998829840842803
评论
请登录