0.1 + 0.2 精度丢失深究

JavaScript 运算符分为许多种,以下就是我们的

JavaScript 运算符种类

算数运算符:+加,-减,*乘,/除,%取余,-(一元取反,也可以说是负),++自加,--自减。

等同全同运算符:==、===、!==、!===

比较运算符:>、<、>=、<=

字符串运算符:>,<,<=,>=,=,+

逻辑运算符:&&、 ||、 !

赋值运算符:=、+=、*=、-=、/=

位运算符:&(与运算)、|(或运算)、^(异或运算)、~(非运算)、>>(带符号的右位移)、>>>(无符号的(用 0 补足的)右位移)、<< (左位移)

今天我们要说的是 JavaScript 中一个比较奇怪的现象,我们直接看下面例子。

console.log(0.1 + 0.2); // 0.30000000000000004
console.log(0.1 + 0.3); // 0.4
console.log(0.2 + 0.3); // 0.5
console.log(0.15 + 0.15); // 0.3

我们发现一个非常奇怪的现象,0.1 + 0.2 != 0.3,而 0.15 + 0.15 却等于了 0.3,这是为什么呢? 这个问题我非常喜欢拿来当做面试题对前端面试者的考察。

0.1 + 0.2 不等于 0.3 的原因

首先让我们了解一下 JavaScript 中的 Number 类型。

在 JavaScript 中,整数和浮点数都属于 Number 数据类型,所有的数字都是以 64 位浮点数形式储存,也就是双精度浮点数,即便是整数也是如此。

十进制小数转换为二进制小数,用 2 乘 10 进制小数,可以得到积,将积的整数部分取出,再用 2 乘余下的小数部分,又得到一个积, 再讲积的整数部分取出,如此进行,知道积中的小数部分为零,此时 0 或 1 为二进制的最后一位。或者达到所要求的精度为止。

下面看两个例子:

0.3 = (0.0 1001 1001...)B
0.3 * 2 = 0.6======取出整数部分0
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
0.4 * 2 = 0.8======取出整数部分0
0.8 * 2 = 1.6======取出整数部分1

0.2 = 0.00110011....
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

 // 0.1 转化为二进制
0.0 0011 0011 0011 0011...(0011无限循环)

由于尾数只有 52 位,所以对于 0.1 和 0.2 转换后的二进制如下:

// S是符号位,在第0位, P是指数位,用e表示,是第1位到第11位。尾数是储存小数部分(即有效数字),第12到63位,用f表示。
e = -4; m =1.1001100110011001100110011001100110011001100110011010 (52位)
e = -3; m =1.1001100110011001100110011001100110011001100110011010 (52位)

像十进制数有45入的规则一样,二进制也存在类似的规则,简单的说,如果 1.101
要保留一位小数,可能的值是 1.11.2,那么先看 1.1011.1 或者 1.2 哪个值更
接近,毫无疑问是 1.1,于是答案是 1.1。那么如果要保留两位小数呢?很显然要么
1.10 要么是 1.11,而且又一样近,这时就要看这两个数哪个是偶数(末位是偶
数),保留偶数为答案。综上,如果第 52 bit 和 53 bit 都是 1,那么是要进位的。
这也导致了误差的产生。

我们看下这两个二进制相加

  e = -4; m = 1.1001100110011001100110011001100110011001100110011010 (52位)
+ e = -3; m = 1.1001100110011001100110011001100110011001100110011010 (52位)
---------------------------------------------------------------------------
相加时如果指数不一致,需要对齐,一般情况下是向右移,因为最右边的即使溢出了,损失的精度远远小于左边溢出。
  e = -3; m = 0.1100110011001100110011001100110011001100110011001101
+ e = -3; m = 1.1001100110011001100110011001100110011001100110011010
---------------------------------------------------------------------------
  e = -3; m = 10.0110011001100110011001100110011001100110011001100111
---------------------------------------------------------------------------
  e = -2; m = 1.0011001100110011001100110011001100110011001100110100(52位)
---------------------------------------------------------------------------
= 0.010011001100110011001100110011001100110011001100110100
= 0.30000000000000004(十进制)

我们可以看到,当十进制小数的二进制表示的有限数字超过 52 位时,在 JavaScript 里是不能精确存储的,这时候就存在舍入误差(Round-off error)。

那么我们如何解决呢?

  • 开源的库:bigInt

  • Number.toFixed 保留指定长度的小数,会四舍五入,不够准确,但可以解决 0.1+0.2 的问题。

  • 各自乘以 10 的 N 次方后,再处于 10 的 N 次方。 N > 1。

  • Number.EPSILON

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

由浮点数精度衍生出来的还有 JAVA 的 long 类型,当超出一定范围后会精度丢失。 我们通过在请求接口的时候,对数据进行一次处理

String.prototype.getLong = function () {
  const text = this.replace(/([^\\]":)(\d*)(,|})/g, '$1"$2"$3');
  return JSON.parse(text);
};

参考文章及发现的有趣 coder:

0.30000000000000004.com/

js 双精度浮点数

最后更新于