重学js系列——数字

473 阅读8分钟

本文的“导火索”由搞java的老弟“炸”出来的。

火车上的发问

他问我有没有出现这样的情况:后端接口返回的数字过大,前端接收到的与后端的不一致。
开始听并不是很理解他的意思,细问后,原来做分布式用到19位雪花ID,传递到前端,会导致精度丢失!
听明白之后,我只知道js小数的精度丢失,难道数大也会出现?

70B077B3.png


随后的反应是js的取值范围,本“大家”知道js 的数字(Number)采用的是64位双精度浮点型,根据8位最大可表示 256(2^8)个数,取值范围为 -128 ~ 127,所以按照这个算法,2^64具体多少也没算,寻思怎么着也超过19位了,不应该存在他说的这种情况啊。

带着这个疑问,写了个接口测试了一下确实!当我输入到18位的时候,就已经不准确了。

image.png

于是,本“大家”决定要探个究竟 🤔

Number

第一步,凭借上次学习位运算的印象,打开了MDN,找到Number这一章节

image.png

文中说明,js 能够准确表示的整数范围在 -2^53~2^53之间,打开计算器换成具体数值为 -9007199254740992~9007199254740992。

属性和方法

image.png

image.png

接着打开ECMAScript 标准,详情如下(看着实在费劲😵)

image.png

20191224114825.gif

看到这样的专业术语英文,再考虑一下我的英文水平,却步,只能翻译+猜,大概读了一下,但是结果还是一知半解😵
接着又搬出我的宝典大犀牛《JavaScript权威指南》,找到Number这一章节有相关介绍,暗自高兴。

文中介绍:

JavaScript不区分整数值和浮点数值,所有数字均用浮点数值表示。JavaScript采用IEEE 754标准定义的64位浮点格式表示数字,这意味着它能表示最大值是 ±1.7976931348623157x10^308,最小值是±5x10^-324。 按照JavaScript中的数字格式,能够表示的整数范围是-9007199254740992~9007199254740992(即-2^53~2^53),包含边界值。如果使用了超过此范围的整数,则无法保证低位数字的精度。

看完这三份文献,都有表明js的数字的类型,整数取值范围这些定义、结论。
此时,我有几个问题:

1.为什么整数范围是 2的53次方,而不是其他次方?
2.超过的数应该如何表示?

上面除此之外也都提到了IEEE 754标准定义的浮点格式并且其他语言的浮点型格式也遵循这一标准。
根据上面2个问题,又是一顿找资料!

E79DD0117A25BDEF3EF602B390C30E9B.jpg

IEEE 754 标准

由于我这里本地打不开IEEE 754标准文档,但是我找别人保存下来了,IEEE 754 - Wikipedia.htm需要的小伙伴保存下来在查看。
下面内容转载于blog.csdn.net/wallc/artic…

组成

浮点格式可分为符号位s,指数位e以及尾数位f三部分。
其中真实的指数E相对于实际的指数有一个偏移量,所以E的值应该为e-Bias,Bias即为指数偏移量。这样做的好处是便于使用无符号数来代替有符号的真实指数。尾数f字段代表纯粹的小数,它的左侧即为小数点的位置。规格化数的隐藏位默认值为1,不在格式中表达。

在IEEE-754 标准下,浮点数一共分为:

  • NaN:即Not a Number。非数的指数位全部为1 同时尾数位不全为0。在此前提下,根据尾数位首位是否为1,NaN 还可以分为SNaN 和QNaN 两类。前者参与运算时将会发生异常。
  • 无穷数:指数位全部为1 同时尾数位全为0。大。
  • 规格化数:指数位不全为1 同时尾不全为0。此时浮点数的隐含位有效,其值为1。
  • 非规格化数:指数位全为0 且尾数位不全为0。此时隐含位有效,值为0。另外需要注意,以单精度时为例,真实指数E 并非0-127=-127,而是-126,这样一来就与规格化下最小真实指数E=1-127=-126 达成统一,形成过度。
  • 0 :指数位与尾数位都全为0,根据符号位决定正负。

浮点的舍入模式

在存储单元的物理限制下,无限精度的浮点数需要根据需求进行舍入操作,一般
可分为四类:

1.最近舍入,即向距离最近的浮点数舍入,若存在两个同样接近的数,则选择偶数作为舍入值。
2.向零舍入,又称截断舍入,将多余的精度位截掉,即取舍入后绝对值较小的值。
3.正向舍入,也称正无穷舍入,即舍入后结果大于原值。
4.负向舍入:也称负无穷舍入,即舍入后结果小于原值。

所以,对于js来说,浮点格式分为 符号位s(最高位),指数为e(符号位后11位),尾数f(指数位后52位)。这也就是上面为什么说,是2的52次方了,而超过范围的值,用Infinity:无穷数,分为正负无穷数。

为什么0.1+0.2≠0.3?(Number的计算)

这其中的原因是二进制小数的储存和计算问题

十进制换算二进制

二进制满二进一。十进制2就是10。

  • 整数

除二取余法:除以2,直到商为0结束,把余数从后到前排列。

举例
十进制:15

十进制被除数 运算符 除数 余数
15 ÷ 2 7 1
7 ÷ 2 3 1
3 ÷ 2 1 1
1 ÷ 2 0 1

把余数从下到上排列就是二进制的值 1111

  • 小数

乘二取整法:小数部分乘以2,进位到整数位取出,继续乘以2,直到位数限制,最后将整数位从先到后排列。

举例

  • 可除尽的十进制小数:0.125

0.125 x 2 = 0

乘数(整数部分) 乘数(小数部分) 运算符 乘数 积(整数部分) 积(小数部分)
0 125 x 2 0 25
0 25 x 2 0 5
0 5 x 2 1 0


把整数部分的积从上到下排列0.001

  • 除不尽的小数:0.1
乘数(整数部分) 乘数(小数部分) 运算符 乘数 积(整数部分) 积(小数部分)
0 1 x 2 0 2
0 2 x 2 0 4
0 4 x 2 0 8
0 8 x 2 1 6
0 6 x 2 1 2
0 4 x 2 0 8
0 8 x 2 1 6
0 6 x 2 1 2
0 2 x 2 0 4
0 4 x 2 0 8
0 8 x 2 1 6
.......无限循环


整数部分从上到下排列是0.000 1100 1100 .......
所以只能存到最大限度64位,剩下的就省略。

计算

js数值计算是化成二进制然后在进行计算,由于篇幅较长,也有大片的百科资料,本文只讲一下原理原因,不做
赘述了,详细可自行百科。

计算出现精度问题就是 由于这些除不尽的小数化成二进制只保留到可计算的位数然后进行计算,可想而知,两个无限小数相加也是无限小数,这一存一取就出现了精度问题,所以像这类 0.1+0.2≠0.3 就是这个原因

超过最大整数的取值范围,前后端交互的解决办法

百度了一顿,看了下各种解决方案,大部分都是让后端把Long类型的大数字转换成字符串String,进行传递,有少部分的是在前端进行解决,详细看了一下代码,太麻烦了,说实话,对于这方面挺失望的,js,感觉是真low,这里推荐用中间件bigNumber.js 来解决

需要了解的知识点与总结

1.JavaScript采用IEEE 754标准定义的64位浮点格式表示数字
2.浮点格式分为 符号位s(最高位),指数为e(符号位后11位),尾数f(指数位后52位)
3.最大值是 ±1.7976931348623157x10^308,最小值是±5x10^-324
4.整数范围是-9007199254740992~9007199254740992(即-2^53~2^53),属于安全整数,超过安全整数的会出现低位丢失精度
5.小数计算推荐bigNumber.js,或将小数化整数进行计算,在化小数

参考

《JavaScript权威指南》
《IEEE 754标准》
《JavaScript Number MDN》
《赵昊鹏的博客》
《Demon's Blog》
《QAWRA》

最后

如果有疑问或不同意见,评论区见,帮助到你,记得关注,加点赞,感谢!