写在前面
这个问题其实一直存在,我也看了很多博主写的文章,但是没有一篇文章真的说明白了这个问题,所以今天我尽量将这个问题讲明白,废话不多说,开整
问题表象
研究一下0.3 - 0.2 不等于0.1的问题,做前端时间久的人都避不开精度缺失的问题,今天我们就研究透他,关于0.3 - 0.2 = 0.09999999999999998 这个问题
其实这个问题不是javascript独有的,很多语言都有这个问题,下面是我用不同语言打印出来的0.3 - 0.2 的问题
- Java
public class HelloWorld {
public static void main(String []args) {
System.out.println(0.3- 0.2);
// 0.09999999999999998
}
}
- Python
#!/usr/bin/python
# Write Python 3 code in this online editor and run it.
print(0.3 - 0.2);
# 0.09999999999999998
- Swift
/* Write swift code in this online editor and run it. */
var myString = 0.3-0.2
print(myString)
/* 0.09999999999999998*/
- TS
const hello : number = 0.3 - 0.2
console.log(hello)
// 0.09999999999999998
- Rust
fn main() {
println!("{}",0.3-0.2); 0.09999999999999998
}
问题分析
要解释这个问题其实也不复杂,就是解释一下计算机和不同的语言之间是怎么交流的,我们要明白一件事就是不管语言本身有多高级,表象都是按照语言本身的语法规则进行开发我们认为计算机可以看得懂的逻辑,但是其实本质是这门语言按照他之前定好的规则进行进制的转换,最终转为计算机看得懂的二进制,也就是说计算机本身只认识0和1,这就是为什么说计算机是由0和1组成的,知道了这个本质上面的事情就好解释了,下面我们拿javascript进行展示,
当我们输入0.3给计算机的时候,他会转成二进制,转换结果为:
代码语言:javascript复制0.3.toString(2) // 0.010011001100110011001100110011001100110011001100110011
这个时候我们发现他是一个无限循环小数,我们计算机数据交互式先存储,再读取,既然是存储,那么就意味着存储的空间是有限的,那么一个无限循环的小数是不可能一直被存储的,所以计算机只能做一个切断的处理,具体切断多少位呢?这里可以参考IEEE754(国际规定的舍入规则,说人话就是avaScript 中进行浮点数运算时,只有前 15 到 17 位是精确的,超出这个范围的数字可能会出现精度损失), 那么切断之后我们保留小数位数多一点,将他的精度扩大之后可以发现(我们保留17位)
代码语言:javascript复制0.2.toPrecision(17) //0.20000000000000001
0.3.toPrecision(17) //0.29999999999999999
看到上面的两个数据,我们所谓的0.3 - 0.2 在计算机里面是 0.29999999999999999 - 0.20000000000000001 = 0.09999999999999998
这就是为什么这个结果值是0.09999999999999998的原因。
为什么不是所有的小数都这样呢?其实这个也很好解释,只要转为二进制之后不是无限循环或者无限不循环的小数都不会有问题,比如0.5 转为二进制之后就是0.1 ,所以他的计算不会出问题
解决办法
js 可以使用第三方库进行处理,比如decimaljs或者BigNumberjs,当然不想引入的话,也可以直接将原始数据放大之后缩小即可,其实本质就是转为整数进行处理,因为整数没有这个问题,比如
代码语言:javascript复制(0.3 * 1000 - 0.2 * 1000)/1000 // 0.1
他的运行我们也可以验证
代码语言:javascript复制(0.3 * 1000).toPrecision(17) // 300.00000000000000
(0.2 * 1000).toPrecision(17) // 200.00000000000000
(300.00000000000000 - 200.00000000000000) / 1000 // 0.1
问题影响
这个问题其实是一个比较严重的问题,特别是银行行业,我们平常写业务代码的时候不注意这个没关系,因为最后最多就是几分钱的差异,但是任何小事都经不过放大,银行的金额一般都是数量比较大的,所以当一个小的差异就很可能被无限放大,最后就会差别很离谱,所以这个还是需要注意的,上面说了decimaljs是可以处理的,那么看了他的readme文件,大概可以推测出来他其实就是将我们的小数转为了string类型的字符串进行处理,这样可以最大程度保持原始值。所以这个问题还是需要注意的。
decimal.js用法
代码语言:javascript复制// 引入CDN https://cdnjs.cloudflare.com/ajax/libs/decimal.js/10.4.3/decimal.min.js
let a = new Decimal(0.3)
let b = new Decimal(0.2)
console.log(a.minus(b).toString()) // 0.1 // 包括加法(plus)、减法(minus)、乘法(times)、除法(div)、乘方(pow)、开方(sqrt)