开端
我们在前端的开发过程中,你一定遇到过这样计算不准确的问题,前端的某一些小数相加,结果尽然是不正确的,有一些却又是正确的,我们搜索引擎检索一番,得知这个叫精读丢失,你是否知道为什么偶尔两个小数相就会出问题,这个问题到底是怎么产生的呢?本文将为你揭秘。
一个叫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个小数位
双精度浮点数(共64位): - 1个符号位 - 11个指数位 - 52个小数位
公式如下:
(-1)^S * M * 2^E 偏移量如下图所示:
需要注意的是在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中的常用的方法如下:
- 为使用类库,如bignumber.js和decimal.js解决精度问题。
- 对于数据运算类 先将小数转成整数再运算。
- 还有不少解决方案,就不一一列举了。