> For the complete documentation index, see [llms.txt](https://tech.banchengyun.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://tech.banchengyun.com/archives/frontend/doublefloat.md).

# 0.1 + 0.2 精度丢失深究

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

### JavaScript 运算符种类

> 算数运算符：+加，-减，\*乘，/除，%取余，-（一元取反，也可以说是负），++自加，--自减。
>
> 等同全同运算符：==、===、!==、!===
>
> 比较运算符：>、<、>=、<=
>
> 字符串运算符：>，<，<=，>=，=，+
>
> 逻辑运算符：&&、 ||、 !
>
> 赋值运算符：=、+=、\*=、-=、/=
>
> 位运算符：&（与运算）、|（或运算）、^（异或运算）、\~（非运算）、>>（带符号的右位移）、>>>（无符号的(用 0 补足的)右位移）、<< （左位移）

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

```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 位浮点数形式储存，也就是双精度浮点数，即便是整数也是如此。

![float](https://www.z4a.net/images/2019/10/28/image.png)

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

下面看两个例子：

```javascript
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 转换后的二进制如下：

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

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

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

```javascript
  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

```javascript
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 类型，当超出一定范围后会精度丢失。 我们通过在请求接口的时候，对数据进行一次处理

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

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

[0.30000000000000004.com/](http://0.30000000000000004.com/)

[js 双精度浮点数](https://www.jianshu.com/p/e071e1da8dfd)
