平时在写js代码时会用到一些简单的计算,比方说系统中我们数据库储存的金额是分,前端展示的是元,所以在用户输入元之后要转成分传给后台,这个公式小学一年级就学过了
1 | 1.11*100 = 111 |
一般来说这个计算结果是没问题的,但是在js里面却有这样的尴尬
1 | 1.11*100 = 111.00000000000001 |
结果不是我们想要的111,类似的情况还有
1 | 0.1+0.2 = 0.30000000000000004 //加法 |
一般遇到这种问题,我们都有成熟的解决方案解决
用着用着就习惯了,一直没有搞清楚为什么会有这样的误差。这两天正好有空,看了一些博客终于搞清楚了。
双精度浮点数
JS 的数据类型比较特别,和C、Java
等语言的的数据类型不一样,不管是 int、double、float
在 JS 里面都是Number
类型。
要搞清楚为什么有这个误差,就要先介绍一下双精度浮点(double)
双精度浮点数(double)使用 64 位(8字节) 来储存一个浮点数。 它可以表示十进位制的15或16位有效数字,其可以表示的数字的绝对值范围大约是 [2.23*10^(-308),1.79*10^(308)]
。
这其中的64位bit又可以分为下面的格式
- sign bit(符号位):0代表正数,1代表负数
- exponent(指数):中间的11位用来表示次方数
- mantissa(尾数):最后的52位用来表示精确度
上面的格式可以转换成这个这个公式
在十进制中,整数部分可以是09,在二进制中整数部分只能是01,所以可以看到上面公式对应的整数部分只能是1,这样就可以不用管整数部分直接保留后面的小数部分就可以了。指数 exponent(E) 是一个无符号整型 (unsigned int) ,那么问题就来了,我们怎么保留小数呢?按照科学计数法,如果E小于0才可以表示成小数,因为E是11位的,最大可以表示为2047,所以取一个中间值1023(十六进制为ox3FF),01022表示为负,10232047表示为正,这样就解决了小数的表示问题。
我们来看看数字 1 是怎么储存的
用上面的公式表示就是:(-1)^0 * 2^(1024-1023) * 1.0 = 1
,再看一下 0.5 的储存形式
(-1)^0 * 2^(1022-1023) * 1.0 = 0.5
,搞清楚这个,我们再看看上面提到的 1.11*100 = 111.00000000000001
这个问题。
将 1.11 转换成二进制是这样的1.0001110000101000111101011100001010001111010111000011...
(11100001010001111010循环)(十进制小数转二进制方法),换成64位浮点来表示,S为0,E为1023,mantissa(M)为0001110000101000111101011100001010001111010111000011,因为位数只有52位,后面循环的部分就被舍弃了,转成64位浮点数是这样的
然后转成10进制的就变成了
1 | 1.11000000000000009769962616701 |
所以这里出现了问题,误差就有了,究其根本还是精度的问题。
还有一个问题?
为什么我直接输入 1.11 得到的结果是 1.11,而不是1.11000000000000009769962616701 呢?
这个还是精度问题,64位浮点的尾数是52位,因为整数部分只能是1所以可以省略一位,比方说
1 | 11.101 * 2^1001 可以格式化为 1.1101 * 2^1010,尾数部分M直接储存1101即可; |
所以他可以表示的最大长度是53,即2^53 = 9007199254740992,所以双精度浮点能表示的最大精度是 16 位的,JS 会调用 toPrecision(16) 来做运算
1 | 1.11.toPrecision(16) = 1.110000000000000 //自动取整之后就是1.11 |
如果精度调整一下,结果就不一样了:
1 | 1.11.toPrecision(17) = 1.1100000000000001 |
到这里终于真相大白了!