# JavaScript 数字精度丢失

由于计算机的二进制实现和位数限制导致有些数字无法有限表示，就像 `10 / 3` 的结果无法表示一样。

## IEEE 754

为了浮点数据处理对于硬件、软件或者两者的结合都能产生独立的结果，不受平台的影响，IEEE 为执行浮点运算提供了一个统一标准，其规定了浮点数的表示格式、操作方式、舍入模式及异常处理。

[IEEE 754](https://zh.wikipedia.org/wiki/IEEE_754) 规范，是 IEEE 浮点格式的基础。

> 大多数编程语言的都基于这个规范。

![IEEE 754](https://img-blog.csdn.net/20170524111255109?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvd2FsbGM=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)

在 IEEE-754 标准下，浮点数一共分为：

* **NaN**，即 Not a Number。 非数的指数位全部为 1 同时尾数位不全为 0。 在此前提下，根据尾数位首位是否为 1，`NaN` 还可以分为 `SNaN` 和 `QNaN` 两类。 前者参与运算时将会发生异常。
* **无穷数**。指数位全部为 `1` 同时尾数位全为`0`。
* **规格化数**。指数位不全为 `1` 同时尾不全为 `0`。 此时浮点数的隐含位有效，其值为 `1`。
* **非规格化数**。指数位全为 `0` 且尾数位不全为 `0`。 此时隐含位有效，值为 `0`。 另外需要注意，以单精度时为例，真实指数 E 并非 0-127=-127，而是-126，这样一来就与规格化下最小真实指数 E=1-127=-126 达成统一，形成过度。
* **0**。指数位与尾数位都全为 0，根据符号位决定正负。

### 舍入误差

在存储单元的物理限制下，无限精度的浮点数需要根据需求进行舍入操作。 IEEE 浮点格式定义了 4 种不同的舍入方式：

1. 最近舍入，即向距离最近的浮点数舍入，若存在两个同样接近的数，则选择偶数作为舍入值。
2. 向零舍入，又称截断舍入，将多余的精度位截掉，即取舍入后绝对值较小的值。
3. 正向舍入，也称正无穷舍入，即舍入后结果大于原值。
4. 负向舍入：也称负无穷舍入，即舍入后结果小于原值。

## JavaScript Number

JavaScript 的数字类型（Number）遵循 IEEE 754 规范的\*\*双精度存储（double precision）\*\*标准。

根据 IEEE 754 规范双精度存储数字类型的内存结构如下：

* 1 位表示符号位
* 11 位表示指数
* 52 位表示尾数

双精度存储数字类型占用 64 bit 空间。

所以 JavaScript 数字类型能表示的最大整数也就是： $2^{53} = 9007199254740992 $

所以，当数字越界的时候,就会出现精度丢失的现象：

```js
console.log(0.1 + 0.2); // 0.30000000000000004
// (0.1).toString(2) => "0.0001100110011001100110011001100110011001100110011001101"
//              越界 => "0.0001100110011001100110011001100110011001100110011001"
// (0.2).toString(2) => "0.001100110011001100110011001100110011001100110011001101"
//              越界 => "0.0011001100110011001100110011001100110011001100110011"
// (0.1 + 0.2) => "0.0100110011001100110011001100110011001100110011001101"
//             => 0.30000000000000004

console.log(0.3 - 0.2); // 0.09999999999999998

console.log(8.7 * 100); // 869.9999999999999
console.log(8.8 * 100); // 880.0000000000001

console.log(0.2 / 1000000); // 2.0000000000000002e-7
console.log(0.2 / 1000000 === 0.0000002); // false

console.log(9999999999999999 === 10000000000000001); // true
```

## 解决方案

无论是 `toFixed()` 还是先乘倍数再缩小回原来的倍数，依旧无法完美的解决这个问题

```js
(0.1 + 0.2).toFixed(1); // '0.3'
(1.335).toFixed(2); // 1.33

(0.1 * 10 + 0.2 * 10) / 10; // 0.3
```

完美的解决思路应该是将浮点数拆分成 \[`Number.MIN_SAFE_INTEGER`, `Number.MAX_SAFE_INTEGER`] 区间内的值再进行计算。

可以参考以下库的实现方式：

* [bignumber.js](https://github.com/MikeMcl/bignumber.js)
* [decimal.js](https://github.com/MikeMcl/decimal.js)
* [big.js](https://github.com/MikeMcl/big.js)
* [safe-float](https://github.com/vincenting/safe-float)

## 参考

* [IEEE 754 浮点数标准详解](http://c.biancheng.net/view/314.html)


---

# 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://xyy94813.gitbook.io/x-note/javascript/javascript-numeric-precision.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.
