# 浮点数精度问题
## 经典真题
- 为什么 *console.log(0.2+0.1==0.3)* 得到的值为 *false*
## 浮点数精度常见问题
在 *JavaScript* 中整数和浮点数都属于 *number* 数据类型,所有数字都是以 *64* 位浮点数形式储存,即便整数也是如此。 所以我们在打印 *1.00* 这样的浮点数的结果是 *1* 而非 *1.00* 。
在一些特殊的数值表示中,例如金额,这样看上去有点别扭,但是至少值是正确了。
然而要命的是,当浮点数做数学运算的时候,你经常会发现一些问题,举几个例子:
**场景一**:进行浮点值运算结果的判断
```js
// 加法
console.log(0.1 + 0.2); // 0.30000000000000004
console.log(0.7 + 0.1); // 0.7999999999999999
console.log(0.2 + 0.4); // 0.6000000000000001
console.log(2.22 + 0.1); // 2.3200000000000003
// 减法
console.log(1.5 - 1.2); // 0.30000000000000004
console.log(0.3 - 0.2); // 0.09999999999999998
// 乘法
console.log(19.9 * 100); // 1989.9999999999998
console.log(19.9 * 10 * 10); // 1990
console.log(9.7 * 100); // 969.9999999999999
console.log(39.7 * 100); // 3970.0000000000005
// 除法
console.log(0.3 / 0.1); // 2.9999999999999996
console.log(0.69 / 10); // 0.06899999999999999
```
**场景二**:将小数乘以 *10* 的 *n* 次方取整
比如将钱币的单位,从元转化成分,经常写出来的是 *parseInt(yuan\*100, 10)*
```js
console.log(parseInt(0.58 * 100, 10)); // 57
```
**场景三**:四舍五入保留 *n* 位小数
例如我们会写出 *(number).toFixed(2)*,但是看下面的例子:
```js
console.log((1.335).toFixed(2)); // 1.33
```
在上面的例子中,我们得出的结果是 *1.33*,而不是预期结果 *1.34*。
## 为什么会有这样的问题
似乎是不可思议。小学生都会算的题目,*JavaScript* 不会?
我们来看看其真正的原因,到底为什么会产生精度丢失的问题呢?
计算机底层只有 *0* 和 *1*, 所以所有的运算最后实际上都是二进制运算。
十进制整数利用辗转相除的方法可以准确地转换为二进制数,但浮点数呢?
*JavaScript* 里的数字是采用 *IEEE 754* 标准的 *64* 位双精度浮点数。
先看下面一张图:

该规范定义了浮点数的格式,对于 *64* 位的浮点数在内存中的表示,最高的 *1* 位是符号位,接着的 *11* 位是指数,剩下的 *52* 位为有效数字,具体如下:
- 符号位 *S*:第 *1* 位是正负数符号位(*sign*),*0* 代表正数,*1* 代表负数
- 指数位 *E*:中间的 *11* 位存储指数(*exponent*),用来表示次方数
- 尾数位 *M*:最后的 *52* 位是尾数(*mantissa*),储存小数部分,超出的部分自动进一舍零
也就是说,浮点数最终在运算的时候实际上是一个符合该标准的二进制数
符号位决定了一个数的正负,指数部分决定了数值的大小,小数部分决定了数值的精度。
*IEEE 754* 规定,有效数字第一位默认总是 *1*,不保存在 *64* 位浮点数之中。也就是说,有效数字总是 *1.xx…xx* 的形式,其中 *xx…xx* 的部分保存在 *64* 位浮点数之中,最长可能为 *52* 位。因此,*JavaScript* 提供的有效数字最长为 *53* 个二进制位(*64* 位浮点的后 *52* 位 + 有效数字第一位的 *1*)。
既然限定位数,必然有截断的可能。
我们可以看一个例子:
```js
console.log(0.1 + 0.2); // 0.30000000000000004
```
为了验证该例子,我们得先知道怎么将浮点数转换为二进制,整数我们可以用除 *2* 取余的方式,小数我们则可以用乘 *2* 取整的方式。
*0.1* 转换为二进制:
*0.1 \* 2*,值为 *0.2*,小数部分 *0.2*,整数部分 *0*
*0.2 \* 2*,值为 *0.4*,小数部分 *0.4*,整数部分 *0*
*0.4 \* 2*,值为0.8,小数部分0.8,整数部分0
*0.8 \* 2*,值为 *1.6*,小数部分 *0.6*,整数部分 *1*
*0.6 \* 2*,值为 *1.2*,小数部分 *0.2*,整数部分 *1*
*0.2 \* 2*,值为 *0.4*,小数部分 *0.4*,整数部分 *0*
从 *0.2* 开始循环
*0.2* 转换为二进制可以直接参考上述,肯定最后也是一个循环的情况
所以最终我们能得到两个循环的二进制数:
*0.1:0.0001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1100 ...*
*0.2:0.0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 ...*
这两个的和的二进制就是:
*sum:0.0100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 ...*
最终我们只能得到和的近似值(按照 *IEEE 754* 标准保留 *52* 位,按 *0* 舍 *1* 入来取值),然后转换为十进制数变成:
sum ≈ 0.30000000000000004
再例如:
```js
console.log((1.335).toFixed(2)); // 1.33
```
因为 *1.335* 其实是 *1.33499999999999996447286321199*,*toFixed* 虽然是四舍五入,但是是对 *1.33499999999999996447286321199* 进行四五入,所以得出 *1.33*。
在 *Javascript* 中,整数精度同样存在问题,先来看看问题:
```js
console.log(19571992547450991); // 19571992547450990
console.log(19571992547450991===19571992547450992); // true
```
同样的原因,在 *JavaScript* 中 *number* 类型统一按浮点数处理,整数是按最大 *54* 位来算,
- 最大( *253 - 1*,*Number.MAX_SAFE_INTEGER*、*9007199254740991*)
- 最小( *-(253 - 1)*,*Number.MIN_SAFE_INTEGER*、*-9007199254740991*)
所以只要超过这个范围,就会存在被舍去的精度问题。
当然这个问题并不只是在 *Javascript* 中才会出现,几乎所有的编程语言都采用了 *IEEE-754* 浮点数表示法,任何使用二进制浮点数的编程语言都会有这个问题。
只不过在很多其他语言中已经封装好了方法来避免精度的问题,而 *JavaScript* 是一门弱类型的语言,从设计思想上就没有对浮点数有个严格的数据类型,所以精度误差的问题就显得格外突出。
通常这种对精度要求高的计算都应该交给后端去计算和存储,因为后端有成熟的库来解决这种计算问题。
前端也有几个不错的类库:
***Math.js***
*Math.js* 是专门为 *JavaScript* 和 *Node.js* 提供的一个广泛的数学库。它具有灵活的表达式解析器,支持符号计算,配有大量内置函数和常量,并提供集成解决方案来处理不同的数据类型。
像数字,大数字(超出安全数的数字),复数,分数,单位和矩阵。 功能强大,易于使用。
***decimal.js***
为 *JavaScript* 提供十进制类型的任意精度数值。
***big.js***
不仅能够支持处理 *Long* 类型的数据,也能够准确的处理小数的运算。
## 真题解答
- 为什么 *console.log(0.2+0.1==0.3)* 得到的值为 *false*
> 参考答案:
>
> 因为浮点数的计算存在 *round-off* 问题,也就是浮点数不能够进行精确的计算。并且:
>
> - 不仅 *JavaScript*,所有遵循 *IEEE 754* 规范的语言都是如此;
> - 在 *JavaScript* 中,所有的 *Number* 都是以 *64-bit* 的双精度浮点数存储的;
> - 双精度的浮点数在这 *64* 位上划分为 *3* 段,而这 *3* 段也就确定了一个浮点数的值,*64bit* 的划分是“*1-11-52*”的模式,具体来说:
> - 就是 *1* 位最高位(最左边那一位)表示符号位,*0* 表示正,*1* 表示负;
> - *11* 位表示指数部分;
> - *52* 位表示尾数部分,也就是有效域部分
-*EOF*-