# 浮点数精度问题

By [Murraya](https://paragraph.com/@murraya) · 2022-05-11

---

浮点数精度问题
=======

经典真题
----

*   为什么 _console.log(0.2+0.1==0.3)_ 得到的值为 _false_
    

浮点数精度常见问题
---------

在 _JavaScript_ 中整数和浮点数都属于 _number_ 数据类型，所有数字都是以 _64_ 位浮点数形式储存，即便整数也是如此。 所以我们在打印 _1.00_ 这样的浮点数的结果是 _1_ 而非 _1.00_ 。

在一些特殊的数值表示中，例如金额，这样看上去有点别扭，但是至少值是正确了。

然而要命的是，当浮点数做数学运算的时候，你经常会发现一些问题，举几个例子：

**场景一**：进行浮点值运算结果的判断

    // 加法 
    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)_

    console.log(parseInt(0.58 * 100, 10)); // 57
    

**场景三**：四舍五入保留 _n_ 位小数

例如我们会写出 _(number).toFixed(2)_，但是看下面的例子：

    console.log((1.335).toFixed(2)); // 1.33
    

在上面的例子中，我们得出的结果是 _1.33_，而不是预期结果 _1.34_。

为什么会有这样的问题
----------

似乎是不可思议。小学生都会算的题目，_JavaScript_ 不会？

我们来看看其真正的原因，到底为什么会产生精度丢失的问题呢？

计算机底层只有 _0_ 和 _1_， 所以所有的运算最后实际上都是二进制运算。

十进制整数利用辗转相除的方法可以准确地转换为二进制数，但浮点数呢？

_JavaScript_ 里的数字是采用 _IEEE 754_ 标准的 _64_ 位双精度浮点数。

先看下面一张图：

![preview](https://storage.googleapis.com/papyrus_images/ef5edc858bd4141f55f21425b9dbc02e09f707b33e929405a942b490b90fb473.png)

preview

该规范定义了浮点数的格式，对于 _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_）。

既然限定位数，必然有截断的可能。

我们可以看一个例子：

    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

再例如：

    console.log((1.335).toFixed(2)); // 1.33
    

因为 _1.335_ 其实是 _1.33499999999999996447286321199_，_toFixed_ 虽然是四舍五入，但是是对 _1.33499999999999996447286321199_ 进行四五入，所以得出 _1.33_。

在 _Javascript_ 中，整数精度同样存在问题，先来看看问题：

    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_\-

---

*Originally published on [Murraya](https://paragraph.com/@murraya/x2PFXxxzb9Z8z0i2q9LE)*
