Javascript精度丢失初探

179 阅读5分钟

开端

我们在前端的开发过程中,你一定遇到过这样计算不准确的问题,前端的某一些小数相加,结果尽然是不正确的,有一些却又是正确的,我们搜索引擎检索一番,得知这个叫精读丢失,你是否知道为什么偶尔两个小数相就会出问题,这个问题到底是怎么产生的呢?本文将为你揭秘。

一个叫Number的数据类型

Number数据类型是javascript基本数据类型之一,和其它语言如Java不同(java中的数据,整数(定点数)用int,浮点数(即小数用float或者double),JavaScript中所有数字包括整数和小数都只有一种类型Number(其实还有一种数据类型BigInt,本文暂不讨论这个问题)。Number的实现遵循 IEEE 754 标准,使用64位固定长度来表示,也就是标准的 double 双精度浮点数(相关的还有float 32位单精度)。

这样的存储结构优点是可以统一处理整数和小数,节省存储空间。

IEEE 754 规范

在IEEE 754标准中浮点数由三部分组成:符号位S(sign bit)(值只有0或者1),偏移后指数E(biased exponent)(阶码),小数M(fraction)(尾码)。浮点数分为两种,单精度浮点数(single precision)和双精度浮点数(double precision),它们两个所占的位数不同。

IEEE754对于浮点数表示方式给出了一种定义。

单精度浮点数(共32位): - 1个符号位 - 8个指数位 - 23个小数位

image.png 双精度浮点数(共64位): - 1个符号位 - 11个指数位 - 52个小数位 image.png

公式如下:

(-1)^S * M * 2^E 偏移量如下图所示:

 preview

需要注意的是在double双精度浮点数中,指数E为0和2047是有特殊含义的,E为0除了表示+0和-0,还表示subnormal numbers. E为2047除了表示+Infinity-Infinity,还表示各种NaN,所以其实际的指数范围为【-1022~1023】.这个就是javascript的Number型数字的定义,接下来,我们来看看浮点数在计算机中是如何计算并存储的。

举个例子

为了帮助理解定义,我来举一个例子,说明小数的转换过程:

如一个10进制浮点数:125.124

1、首先对整数进行转换

  • 采用"除2取余,逆序排列"法:

1.首先用2整除一个十进制整数,得到一个商和余数
2.然后再用2去除得到的商,又会得到一个商和余数
3.重复操作,一直到商为小于1时为止
4.然后将得到的所有余数全部排列起来,再将它反过来(逆序排列)

125/2   余 1

62/2    余0

31/2    余1

15/2    余1

7/2      余1

3/2     余1

1/2      余1

125转换成二进制之后结果取反时

于是(125)10  = 20+22+23+24+ 25+26= (1111101)2

2、对小数部分进行转换

  • 采用"乘2取整,顺序排列"法:

1.用2乘十进制小数,可以得到积,将积的整数部分取出
2.再用2乘余下的小数部分,又得到一个积,再将积的整数部分取出
3.重复操作,直到积中的小数部分为零,此时0或1为二进制的最后一位,或者达到所要求的精度为止

0.124 * 2 = 0.248    取0

0.248 * 2 = 0.496   取0

0.496 * 2 = 0.992   取0

0.992 * 2 = 1.948    取1

1.948 * 2 = 3.896    取1

....

于是(0.124)10 = (0.0001111111.....)2

3、科学计数法

计算中使用科学计数法表示二进制数字。上面的数,小数和整数加起来,125.124的二进制表示就是1111101.0001111111....,这个数的二进制小数位是无穷的,这个数用科学计数法表示就是(1.11110100011111...)* 26,  这个6(110)就是指数,所以在64位浮点数中其偏移后指数E为6+1023=1029,  科学计数法中小数点前的 1 可以省略,因为这一位永远是 1(这就是为什么小数位有52位,却可以表示53位的数值),所以其小数位为11110100011111...

所以其在计算机中的存储为 

0   0100 0000 101  1111 0100 0111 1111 ...

4、整数的范围

除此之外,整数的存储也是有上限的,其范围在-121023(1+(1-2-52))即Number.MIN_VALUE和 121023(1+(1-2-52))即刻Number.MAX_VALUE。为啥Number的最大安全数Number.MAX_SAFE_INTEGER的值为253 - 1 ,那 是因为整数需要连续,而高于53位的数就不能保持连续了。

问题

        我们理解了计算中是如果存储数据的,那么就回到了那个经典问题,小数相加为何出现精度误差,那就是十进制数值,无法用二进制精确描述,其转换一定会有误差。整数相加,因为有最大安全数的限制,所以整数的加减乘除也会出现精度误差。

解决办法

 javascript中的常用的方法如下:

  1. 为使用类库,如bignumber.js和decimal.js解决精度问题。    
  2. 对于数据运算类 先将小数转成整数再运算。
  3. 还有不少解决方案,就不一一列举了。