# 浮点数精度问题 ## 经典真题 - 为什么 *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 *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.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*-