浅谈一下 js 的 Number 、 小数与精度问题
前言
js 中 Number 也是一个比较杂的数字,相比较 c语言
中的 整数、小数 分开,甚至长整型都要分开来说,js 中的 Number 代表的就比较多了,其可以表示整数、小数、大整数
其中整数
是用着最舒服的,小数
和大整数
都会存在精度
问题
number
Number 中的安全数组 Number.MAX_SAFE_INTEGER
为 9007199254740991
也就是 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