2024-08-27 10:14:31 +08:00

243 lines
7.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 浮点数精度问题
## 经典真题
- 为什么 *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* 所以所有的运算最后实际上都是二进制运算。
十进制整数利用辗转相除的方法可以准确地转换为二进制数,但浮点数呢?
<img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9fc493d0e2e84274b8445d8c5df405ae~tplv-k3u1fbpfcp-watermark.awebp" alt="img" style="zoom:50%;" />
*JavaScript* 里的数字是采用 *IEEE 754* 标准的 *64* 位双精度浮点数。
先看下面一张图:
![preview](https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2021-09-29-060439.png)
该规范定义了浮点数的格式,对于 *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.10.0001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1100 ...*
*0.20.0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 ...*
这两个的和的二进制就是:
*sum0.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* 位来算,
- 最大( *2<sup>53</sup> - 1**Number.MAX_SAFE_INTEGER*、*9007199254740991*)
- 最小( *-(2<sup>53</sup> - 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*-