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






网友评论