之前刚入门时处理项目内表格合计时出现了一个问题,在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进行推算

  1. 0.1转换为二进制 => 0.0001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1100 ...

  2. 0.2转换为二进制 => 0.0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 ...

  3. 相加 => 0.0100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100

  4. 按照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

附录:bignumber.js官方文档

Tokials

月亮被嚼碎了变成星星,你就藏在漫天的星光里。

Tokials

月亮被嚼碎了变成星星,你就藏在漫天的星光里。