# 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)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://tech.banchengyun.com/archives/frontend/doublefloat.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
