做前端开发的朋友,应该大部分都知道这么一个奇怪的现象:0.1 + 0.2 不等于 0.3。
1 2 3 4 5 |
```js // 在Google Chrome 版本 78.0.3904.108(正式版本) (64 位) 下; console.log(0.1+0.2) // 0.30000000000000004 ``` |
需要说明的是,这不只是 js 一门语言的问题,只要是使用了 IEEE 754标准来表示数值的语言,都有这个问题。那么 IEEE 754 是怎么一回事呢,我们一起来看看。
1、浮点数
在生活中,我们用到的有整数和小数,计算机中也是如此。整数没有小数,所以在计算机中用定点数表示,而有小数的数,则用浮点数表示。
浮点数:简单的说就是小数点位置可以浮动的数。它采用了科学计数法来表达实数。即用一个有效数字,一个基数(Base)、一个指数(Exponent)以及一个表示正负的符号来表达实数。比如,666.66 用十进制科学计数法可以表达为 6.6666×10^2(其中,6.6666 为有效数字,10 为基数,2 为指数),也可以表示为 66.666×10^1。浮点数利用指数达到了浮动小数点的效果,从而可以灵活地表达更大范围的实数。
同时,IEEE 754标准准确地定义了单精度和双精度浮点格式,并为这两种基本格式分别定义了扩展格式,如下所示:
- 单精度浮点格式(32 位)。
- 双精度浮点格式(64 位)。
- 扩展单精度浮点格式(≥43 位,不常用)。
- 扩展双精度浮点格式(≥79 位,一般情况下,Intel x86 结构的计算机采用的是 80 位,而 SPARC 结构的计算机采用的是 128 位)。
其中,只有 32 位单精度浮点数是本标准强烈要求支持的,其他都是可选部分。下面就来对单精度浮点与双精度浮点的存储格式做一些简要的阐述。
名称 | | | 描述 |
向偶数舍入 | | | 也称为向最接近的值舍入,会将结果舍入为最接近且可表示的值 |
向0舍入 | | | 将结果朝零的方向舍入 |
向上舍入 | | | 向+无穷方向舍入,会将结果朝正无穷大的方向舍入 |
向下舍入 | | | 向-无穷方向舍入,会将结果朝负无穷小的方向舍入 |
下表是 4 种舍入方式的应用举例。这里需要特别说明的是,向偶数舍入(向最接近的值舍入)方式会试图找到一个最接近的匹配值。因此,它将 1.4 舍入成 1,将 1.6 舍入成 2,而将 1.5 和 2.5 都舍入成 2。
或许看了上面的内容你会问:为什么默认是采用向偶数舍入,而不直接使用我们已经习惯的“四舍五入”呢?
其原因我们可以这样来理解:在进行舍入的时候,最后一位数字从 1 到 9,舍去的有 1、2、3、4;它正好可以和进位的 9、8、7、6 相对应,而 5 却被单独留下。如果我们采用四舍五入每次都将 5 进位的话,在进行一些大量数据的统计时,就会累积比较大的偏差。而如果采用向偶数舍入的策略,在大多数情况下,5 舍去还是进位概率是差不多的,统计时产生的偏差也就相应要小一些。
同样,针对浮点数据,向偶数舍入方式只需要简单地考虑最低有效数字是奇数还是偶数即可。例如,假设我们想将十进制数舍入到最接近的百分位。不管用哪种舍入方式,我们都将把 1.2349999 舍入到 1.23,而将 1.2350001 舍入到 1.24,因为它们不是在 1.23 和 1.24 的正中间。另一方面我们将把两个数 1.2350000 和 1.2450000 都舍入到 1.24,因为 4 是偶数。
对二进制而言,这里的向偶数舍入可能不是很好理解,故举例特意说明一下。
· 1.001 011经舍入处理后的结果为1.001。为什么呢?我们可以计算一下舍入后的结果与1.001和1.010的距离。
|1.001011-1.001|=0.000011,而|1.001011-1.010|=0.00101。很明显前者更接近真实值,因此我们选择舍入后的结果为1.001,也即是向下舍入(此时舍入后的结果比真实值小)。
· 1.001101经舍入处理后的结果为1.010。同样的道理,我们来看一下这个1.001和1.010与这个值的距离。
|1.001101-1.001|=0.000101,而|1.001101-1.010|=0.000011。很明显后者更接近真实值,因此这里我们选择舍入后的结果为1.010,也即是向上舍入(此时舍入后的结果比真实值大)。
现在,我们差不多可以发现,当有效位的后一位是0时,此时即将被舍去的值小于最后一位有效位数值的一半,那么应该向下舍入;当有效位的后一位是1时,而且后面的数位不全为0,此时即将被舍去的值大于最后一位有效数值的一半,那么应该向上舍入。
讨论完这两种情况,我们再来看一种特殊情况:有效位后一位是1,后面数位全是0,此时即将被舍去的值刚好是有效位数值的一半,那么应该怎么进行舍入呢?如果始终选择向上或者向下舍入都会使结果比真实值大或者小。因此,这里我们需要选择向偶数舍入,也即是将数字向上或者向下舍入,使得结果的最低有效位是偶数。这样,在50%的时间里,它将向上舍入,而在50%的时间里,它将向下舍入。
·1.001100:距它最近的两个偶数分别是1.000和1.010。|1.001100-1.000|=0.001100,而|1.001100-1.010|=0.000100,很明显后者距离更近。因此我们选择舍入后的结果为1.010。
·1.100100:据它最近的两个偶数分别是1.100和1.110。|1.100100-1.100|=0.000100,而|1.100100-1.110|=0.001100,显然前者距离更近。因此我们选择舍入后的结果为1.100。
从这个例子我们可以归纳出,如果即将被舍的值刚好等于一半,如果最低有效位为奇,则向上舍入,如果为偶,则向下舍入,从而实现使最低有效位始终为偶数。
1 2 3 4 5 6 7 8 9 |
//如:十进制255转二进制 255/2=127=====余1 127/2=63======余1 63/2=31=======余1 31/2=15=======余1 15/2=7========余1 7/2=3=========余1 3/2=1=========余1 1/2=0=========余1 |
所以十进制的 255 转为二进制就是 11111111。
十进制小数转换成二进制小数采用”乘2取整,顺序排列”法。具体做法是:用 2 乘十进制小数,可以得到积,将积的整数部分取出,再用 2 乘余下的小数部分,又得到一个积,再将积的整数部分取出,如此进行,直到积中的小数部分为零,此时 0 或 1 为二进制的最后一位,或者达到所要求的精度为止。
1 2 3 4 |
//如:0.625=(0.101)B 0.625*2=1.25======取出整数部分1余0.25 0.25*2=0.5========取出整数部分0余0.5 0.5*2=1==========取出整数部分1余0,结束 |
1 2 3 4 5 6 7 8 9 10 |
//如:0.7=(0.1 0110 0110...)B 0.7*2=1.4========取出整数部分1 0.4*2=0.8========取出整数部分0 0.8*2=1.6========取出整数部分1 0.6*2=1.2========取出整数部分1 0.2*2=0.4========取出整数部分0 0.4*2=0.8========取出整数部分0 0.8*2=1.6========取出整数部分1 0.6*2=1.2========取出整数部分1 0.2*2=0.4========取出整数部分0 |
1 2 3 4 5 6 7 8 9 10 11 12 |
// 0.1 转二进制的过程分析 0.1 * 2 = 0.2 # 0 0.2 * 2 = 0.4 # 0 // 0.4 * 2 = 0.8 # 0 0.8 * 2 = 1.6 # 1 0.6 * 2 = 1.2 # 1 0.2 * 2 = 0.4 # 0 // 注意,从这里已经出现了循环 0.4 * 2 = 0.8 # 0 0.8 * 2 = 1.6 # 1 0.6 * 2 = 1.2 # 1 ...... // 无限循环,永远都是 0011 |
1 2 3 4 5 6 |
0.1 === 0.100000000000000002 true 0.1 === 0.1000000000000000022 true 0.1 === 0.1000000000000000022222222 true |
上面这些式子之所以成立,就是因为它们在转换成二进制后出现了无限循环,进行舍入后,得到了相同的浮点数表示,所以被判断为相等。
1 2 3 4 5 |
0.1 + 0.2 != 0.3 // true 0.100000000000000002 === 0.1 // true 0.1 + 0.2 // 0.30000000000000004 0.56*100 // 56.00000000000001 1.23 + 1.24 // 2.4699999999999998 |
像这样的问题数不胜数,如果你的程序里面小数操作比较频繁,那么你可一定得要小心了。
1 2 3 4 5 6 7 8 |
Number.EPSILON=(function(){ // 解决兼容性问题,有些浏览器不支持Number.EPSILON return Number.EPSILON?Number.EPSILON:Math.pow(2,-52); })(); function numbersequal(a,b){ return Math.abs(a-b)<Number.EPSILON; } var a=0.1+0.2, b=0.3; console.log(numbersequal(a,b)); //true |
1 2 |
parseFloat((0.1 + 0.2).toFixed(10)) === 0.3 // true console.log(0.1*1000+0.2*1000)/1000 // 0.3 |
发表评论