JS计算误差小谈

平时在写 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 位用来表示精确度
image

img

上面的格式可以转换成这个这个公式

image

img

在十进制中,整数部分可以是 0~9,在二进制中整数部分只能是 0~1,所以可以看到上面公式对应的整数部分只能是 1,这样就可以不用管整数部分直接保留后面的小数部分就可以了。指数 exponent(E) 是一个无符号整型 (unsigned int) ,那么问题就来了,我们怎么保留小数呢?按照科学计数法,如果 E 小于 0 才可以表示成小数,因为 E 是 11 位的,最大可以表示为 2047,所以取一个中间值 1023(十六进制为 ox3FF),0~1022 表示为负,1023~2047 表示为正,这样就解决了小数的表示问题。

我们来看看数字 1 是怎么储存的

image.gif

img

用上面的公式表示就是:(-1)^0 _ 2^(1024-1023) _ 1.0 = 1,再看一下 0.5 的储存形式

image.gif

img

(-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

img

然后转成 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

你可能感兴趣的:(JS计算误差小谈)