之前刚入门时处理项目内表格合计时出现了一个问题,在js的运算中,莫名出现了一堆小数,导致与后台的数据对应不上的问题,最近又看到有童鞋求助,所以总结下我的处理。
1、Bug成因
首先我们应该知道,javascript是一门弱类型的语言,设计思想上就没有对浮点数有个严格的数据类型,所以精度误差这个问题早早就存在了,在其他的语言如java、c#等其实也是有这样的问题,但是不好意思的是,人家java的BigDecimal、c#的decimal内部都对精度问题做了处理,剩下的js还需要开发者做兼容处理。
console.log(Number(0.235).toFixed(2)); //=> 0.23
0.333 + 0.4 // => 0.7330000000000001
大家可能都会遇到这样的问题,比如四舍五入并没有四舍五入,计算多出一堆很长很长的小数,遇到这种情况我们可以大致分析下,又到了理论时间,我们在学习计算机基础的时候就应该听到过:
计算机执行的是二进制算术,当十进制数不能准确转换为二进制数时,就出出现误差,javascript中的数字都是用浮点数表示的,并规定使用IEEE 754 标准的双精度浮点数表示,因此,js在运算小数的时候,需要将小数转为二进制,这时就已经丢失了精度,在计算机中二进制运算完毕,将二进制转为十进制,这里又再次丢失了精度,所以这里我们大概就了解到了Bug的大致原因。
IEEE 754 规定了两种基本浮点格式:单精度和双精度。
单精度格式:具有24 位有效数字精度(包含符号号),并总共占用32 位。
双精度格式:具有53 位有效数字精度(包含符号号),并总共占用64 位。
再次深入一下:
我们使用 0.1 + 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 ...
-
相加 => 0.0100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100
-
按照IEEE 754标准保留 52位,按权相加法, 0舍1入 来取值 => sum ≈ 0.30000000000000004
此时,答案已经呼之欲出,转换的过程中已经失去精度,再怎么算也是丢失了。
2、如何处理
方法一:升级降级
这个其实大家听的挺多,不细说,主要原理就是将小数转为整数,整数运算完成再转回去,那刚刚的例子,0.1 和 0.2 同时乘10,得到 1 和 2 ,相加为3,这时候除以阶乘数,3除以10,得到0.3
const Add = (n1, n2) => {
let pow = Math.max(n1.toString().split(".")[1].length, n2.toString().split(".")[1].length)
let base = Math.pow(10, pow)
return (Number(n1.toString().replace(".", "")) + Number(n2.toString().replace(".", ""))) / base;
}
方法二:自封装处理函数
const computeFloat = {
getDigits (num1, num2) {
let d1, d2
let d1Arr = (num1 + '').split('.')[1]
let d2Arr = (num2 + '').split('.')[1]
d1 = d1Arr ? d1Arr.length : 0
d2 = d2Arr ? d2Arr.length : 0
return { d1, d2 }
},
compute (num1, num2, type) {
let { d1, d2 } = this.getDigits(num1, num2)
switch (type) {
case 'add':
return this.add(num1, num2, d1, d2)
case 'subtract':
return this.subtract(num1, num2, d1, d2)
case 'multiply':
return this.multiply(num1, num2, d1, d2)
case 'divide':
return this.divide(num1, num2, d1, d2)
}
},
add (num1, num2, d1, d2) {
let m = Math.pow(10, Math.max(d1, d2))
return (num1 * m + num2 * m) / m
},
subtract (num1, num2, d1, d2) {
let m = Math.pow(10, Math.max(d1, d2))
return (num1 * m - num2 * m) / m
},
multiply (num1, num2, d1, d2) {
let m = Math.pow(10, d1 + d2)
return ((num1 + '').replace('.', '')) * ((num2 + '').replace('.', '')) / m
},
divide (num1, num2, d1, d2) {
let m = Math.pow(10, d2-d1)
return ((num1 + '').replace('.', '')) / ((num2 + '').replace('.', '')) * m
}
}
方法三:使用第三方库
这才是今天的重点,由于上面两种方法写起来太麻烦,所以我推荐使用成熟的三方库,比如:
Math.js
用于 JavaScript 和 Node.js 的扩展数学库。
它具有支持符号计算的灵活表达式解析器,大量内置函数和常量,并提供了集成的解决方案来处理不同的数据类型,例如数字,大数,复数,分数,单位和矩阵。强大且易于使用。
decimal.js
JavaScript 的任意精度的十进制类型。
big.js
一个小型,快速,易于使用的库,用于任意精度的十进制算术运算。
bignumber.js
一个用于任意精度算术的 JavaScript 库。
3、推荐bignumber.js
sum
计算传入的参数和,参数类型可以是 String,Number
// 两数之和
var x = BigNumber.sum('11', 23)
x.toNumber() // 34
// 多个参数
arr = [2, new BigNumber(14), '15.9999', 12]
var y = BigNumber.sum(...arr)
y.toString() // '43.9999'
maximum,minimum
求最大值,简写max,min
var x = [2222, 3333, '4444']
BigNumber.max(...x).toNumber() // 4444
BigNumber.min(...x).toNumber() // 2222
decimalPlaces(dp)
确定小数位数
var x = new BigNumber(1234.5678912345)
var y = new BigNumber(1234.56)
x.dp(2).toNumber() // 1234.56
y.dp(10).toNumber() // 1234.56
plus
加法运算
0.1 + 0.2 // 0.30000000000000004
var x = new BigNumber(0.1)
x.plus(0.2).toNumber() // 0.3
minus
减法运算
0.3 - 0.1 // 0.19999999999999998
var x = new BigNumber(0.3)
x.minus(0.1) // 0.2
multipliedBy(times)
乘法运算
0.6 * 3 // 1.7999999999999998
var x = new BigNumber(0.6)
x.times(3) // 1.8
dividedBy(div)
除法运算
var x = new BigNumber(300)
x.div(3).toNumber() // 100
x.div(7).dp(3).toNumber() // 42.857
dividedToIntegerBy(idiv)
除法运算,返回整数
var x = new BigNumber(5)
x.idiv(3).toNumber() // 1
x.idiv(0.7).toNumber() // 7
modulo(mod)
取余
1 % 0.9 // 0.09999999999999998
var x = new BigNumber(1)
x.mod(0.9).toNumber() // 0.1
toFixed
控制小数位数,不够后面补0
var x = 3.456
var y = new BigNumber(x)
x.toFixed().toNumber() // 3
y.toFixed().toNumber() // 3.456
y.toFixed(0).toNumber() // 3
y.toFixed(2).toNumber() // 3.46
y.toFixed(5).toNumber() // 3.45600
Comments | NOTHING