前端应该知道的JavaScript浮点数和大数的原理

3,111 阅读6分钟

我是耳东,会写点前端,混过字节跳动、美团,做过面试官,擅长程序员职场话题、算法、前端
有一个不水的技术群,有一个不水的技术群,关注公众号“程序员耳东”拉你进群

2022年上半年准备写一个「刷500题进大厂」系列算法教程

欢迎关注技术公众号:程序员耳东

欢迎关注B站号:程序员耳东

不知道大家在平时的搬砖中有没有遇到过一些JavaScript数字相关的坑,比如比较经典的0.1+0.2=0.30000000000000004、JavaScript有一个Number.MAX_VALUE还有一个Number.MAX_SAFE_INTEGER等等问题。如果这些问题不了解清楚,业务开发中很有可能会出现一些很奇怪的问题。

几个问题

先抛出几个问题

  1. 为什么0.1+0.2 != 0.3?

  2. 为什么1.005.toFixed(2)=1.00而不是1.01

  3. 为什么会有Number.MAX_VALUE和Number.MAX_SAFE_INTEGER这两个常量同时存在?

接下来就以这三个问题为目的来梳理一下来龙去脉。

双精度存储

首先在开始之前需要了解一下JavaScript的number类型在计算机中是如何存储的,这也是一切问题的基础。JavaScript的数字都是number类型的,不管是整数还是浮点数都以IEEE754双精度的格式存储在计算机中,什么是双精度呢?就是以64个bit位来存储,具体的存储格式是:

分别是1个符号位+11个指数位+52个尾数位

举个例子,如果是5.5这个数字的话,则计算过程是这样的:
5.5 转二进制 =====> 101.1 科学计数法 =====> 1.011*2^2 
存入计算机: 
符号位:0 
指数位:2 加1023 =====> 1025 转二进制 =====> 10000000001 
尾数位:1.011 隐去小数点左边的1 =====> 011

存入计算机,如下图,截图来自IEEE754可视化,感兴趣可以把玩一下

接下来进入第一个问题

为什么0.1+0.2 != 0.3?

在浏览器控制台可以测试一下结果:

下面就剥茧抽丝的讲一下为什么会这样

0.1 转二进制 =====> 0.0001100110011001100...(1100循环) 
转科学计数法 =====> 1.100110011...(1100循环) *2^-4 
数据是无限循环的,但是可供使用的尾数位却是有限的,只有52位可以使用,所以在第53位会被舍去并且进位

最终在计算机中存储如下图:

类似的0.2在计算机中的存储如下图:

所以最终的计算就是:

0.00011001100110011001100110011001100110011001100110011010 + 0.0011001100110011001100110011001100110011001100110011010 = 0.0100110011001100110011001100110011001100110011001100111

计算结果转换为十进制数字就是0.30000000000000004

所以就是因为,0.1和0.2在计算机中的二进制存储会让它们本身损失掉一定的精度,而它们在计算机中的二进制存储转换成十进制时已经不是真正的0.1和0.2了,相加的结果也就自然不是0.3了。

问题来了,既然0.1在计算机中的存储已经有了舍入误差,那为什么num=0.1能得到0.1呢?

可以在控制台使用toPrecision看一下0.1在不同精度下的返回

可以看出来其实0.1是截断了一部分精度后得到的结果,那么这个问题就可以转化为:双精度浮点数是按什么规则来截断的呢?

在双精度浮点数的英文wiki中可以找到中可以找到这么一段话:

大意是:如果一个 IEEE 754 的双精度浮点数被转成至少含17位有效数字的十进制数字字符串,当这个字符串转回双精度浮点数时,必须要跟原来的数相同;换句话说,如果一个双精度的浮点数转为十进制的数字时,只要它转回来的双精度浮点数不变,精度取最短的那个就行。

拿0.1来举例子,0.1和0.10000000000000001转成双精度浮点数的存储是一样的,所以取最短的0.1就行了。

接下来是第2个问题

为什么1.005.toFixed(2)=1.00而不是1.01

因为在第一个问题中已经说了,一个十进制数字转为双精度浮点数然后再取出来时,跟原十进制数字可能会有误差,试一下1.005取20个精度:

很明显1.005只是一个被截断后的数字,它的双精度浮点数代表的20位精度的数字是1.0049999999999998934,所以进行保留2位的四舍五入时,2位后的数字会被全部舍去。

为什么会有Number.MAX_VALUE和Number.MAX_SAFE_INTEGER这两个常量同时存在?

可以在控制台看一下:

为什么最大安全整数是2^53-1?前面说到了JavaScript浮点数存储是52位尾数位,但是因为科学计数法小数点左侧的1会在存储时省去,所以52位尾数+省去的1位=53个可表示的位数。

二进制的53位全是1时转换为十进制既是:2^53-1=9007199254740991

哪为什么2^53-1是最大安全整数呢?比它大会怎样?

在浏览器试一下:

以2^53来说明一下为什么2^53-1是最大安全整数,安全在哪里

2^53 转二进制 =====> 100000000000000000000000000000000000000000000000000000(53个0)
转为科学计数法 =====> 1.00000000000000000000000000000000000000000000000000000(53个0)*2^53 
存入计算机 =====> 尾数位只有52位所以截掉末尾的0只能存52个0 
2^53+1 转二进制 =====> 100000000000000000000000000000000000000000000000000001(52个0) 
转为科学计数法 =====> 1.00000000000000000000000000000000000000000000000000001(52个0) 
存入计算机 =====> 尾数位只有52位所以截掉末尾的1只能存52个0

可以看出来,2^53和2^53+1在计算机中的存储尾数和指数都相同,所以两个不同的数在计算机中的存储是一样的,这样就非常的不安全了。

所以2^53-1是JavaScript里面的最大安全整数。至于Number.MAX_VALUE,就是把尾数位和指数位都设为1再转为十进制就好了。

如何解决

上面的说到的浮点数运算、舍入运算、大数运算,都可以使用bignumber这个库来解决,打开链接在控制台就可以写demo。

后记

业务中很多难缠的bug往往是基础知识了解的不够深入造成的,了解这些原理可以清晰的知道自己在写什么,并且快速的debug,不至于陷入cv(control+C、control+V)工程师的循环中。

---------------------------------

欢迎关注我的公众号,程序员耳东