「这是我参与2022首次更文挑战的第12天,活动详情查看:2022首次更文挑战」。
现象
在编程语言中,有一种很奇怪的现象:当小数和小数发生运算的时候,结果跟我们熟识的数学运算结果不同,如下:
var num1 = 0.1
var num2 = 0.2
var num3 = num1 + num2
console.log(num3) // 0.30000000000000004
我们在已有的数学知识基础上,以为结果应该是精准的0.3,可结果却打脸了,如果不懂其中的原理,研究编程会研究到怀疑人生。
这种现象在所有编程语言中都有,javascript也不例外。
原因
计算机的计算跟我们熟知的数学计算原理是一样的,只是计算机内部在存储数据的时候,和我们想象中的不同。
计算机因为是电子元件设备,所以内部的电子元件只有2种状态:通电和断电。也就造成了计算机内部存储数据都是以2进制形式来存储的,用1来描述通电,用0来描述断电。
我们平常所进行的整数计算:
var num1 = 1
var num2 = 2
var num3 = a + b
console.log(num3) // 3
这个结果3,不是单纯的1加上2。在计算机内部会将1以2进制的形式存储起来,将2也以2进制的形式存储起来,然后在计算机内部,将两个2进制数字相加,得到一个2进制的相加结果,然后,再将这个2进制结果转为10进制输出。
同理,小数在计算的时候也是一样的。可是小数转成2进制的时候,因为要对数据乘2直到取整,所以像0.5这种数字,可以很快计算得到2进制结果,但是0.1和0.2这样的数字,是永远不可能计算得到准确的2进制结果的,因为一直乘2,就没有得到整数的时候,此时在转换2进制的过程中,会形成无限死循环。
计算机内部在存储无限死循环数据时,必须要有一个限度,采取舍去的原则,所以,0.1和0.2在计算机内部存储的对应的2进制数字,本来就不精准,所以相加得到的2进制结果,也就不精准了,那转换成10进制后,是会有一定的误差的,所以结果不是精准的0.3。
了解了上面的原因后,再看如下奇怪的计算的结果:
var num1 = 0.1
var num2 = 0.1
var num3 = num1 + num2
console.log(num3) // 0.2
根据上面的原理,我们断定只要是0.1参与的小数运算,一定是不精准的,可是这又怎么解释呢?
事实上,我们理解的没有错,只要是0.1,在计算机内部存储,一定是被舍去后面的死循环的一部分,即不精准的。
但两个不精准的2进制数字,在相加后,得到的2进制数字,转换10进制时,正好就能转换成1个精准的10进制数字了。很神奇吧。
具体转换计算过程如下:
10进制:0.1
转换2进制后:0.0001 1001 1001 1001 ... 无限死循环部分
计算机内部对于2进制小数,根据IEEE754标准(是一个仔细制定的表示浮点数及其运算的标准),小数部分最多会保留52位:
0.0001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001
+
0.0001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001
=
0.0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011
相加以后的结果,正好就对应0.2在计算机内部2进制舍去的部分,所以,转换为10进制后正好就是0.2。
所以,不精准是正确的,只是偶尔会有两个不精准的数字相加,正好得到一个精准的值。
解决
方法一
将需要运算的小数扩大10倍、100倍、。。。将小数扩大到整数,然后进行运行,最后再缩小扩大的倍数。例:
var num1 = 0.1
var num2 = 0.2
var num3 = (num1 * 10 + num2 * 10) / 10
console.log(num3) // 0.3
方法二
通过js中Number的内置方法toFixed,强制保留小数点后位数。例:
var num1 = 0.1
var num2 = 0.2
var num3 = num1 + num2
console.log(num3.toFixed(3)) // 0.300 - 强制保留小数点后3位
方法三
封装数学运算方法,当需要进行数学运算的时候,不直接进行,而调用自己封装的方法来实现数学运算。例:
function add(...args){
var num = args.find(item => {
if(item != 0 && !item){
throw new Error("数学运算要使用数字")
}
})
var arr = args.map(item => {
var index = (item+'').indexOf('.')
if(index >= 0){
return (item+'').split('.')[1].length
}
})
arr = arr.filter(item => item)
if(arr.length){
var max = Math.max(...arr)
var data = args.map(item => item * Math.pow(10, max))
var data.reduce((a, b) => a + b) / Math.pow(10, max)
}else{
var data = args
return data.reduce((a, b) => a + b)
}
}
// 调用使用:
var num1 = add(0.1, 0.2)
console.log(num1); // 0.3
var num2 = add(1, 2)
console.log(num2); // 3
var num3 = add(1, 2.1)
console.log(num3); // 3.1
同理,其他运行也一样封装这样一个函数,以后不直接运算,只要进行数学运算就调用这个方法即可。