解决 JavaScript 中计算精度问题


解决 js 中计算精度的问题

问题描述

在 JavaScript 中整数和浮点数都属于 Number 数据类型,所有数字都是以 64 位浮点数形式储存,即便整数也是如此。 所以我们在打印 1.00 这样的浮点数的结果是 1 而非 1.00 。在一些特殊的数值表示中,例如金额,这样看上去有点变扭,但是至少值是正确了。然而要命的是,当浮点数做数学运算的时候,你经常会发现一些问题,举几个例子:

加法 =====================
0.1 + 0.2 = 0.30000000000000004
0.7 + 0.1 = 0.7999999999999999
0.2 + 0.4 = 0.6000000000000001
2.22 + 0.1 = 2.3200000000000003
 
减法 =====================
1.5 - 1.2 = 0.30000000000000004
0.3 - 0.2 = 0.09999999999999998
 
乘法 =====================
19.9 * 100 = 1989.9999999999998
19.9 * 10 * 10 = 1990
1306377.64 * 100 = 130637763.99999999
1306377.64 * 10 * 10 = 130637763.99999999
0.7 * 180 = 125.99999999999999
9.7 * 100 = 969.9999999999999
39.7 * 100 = 3970.0000000000005
 
除法 =====================
0.3 / 0.1 = 2.9999999999999996
0.69 / 10 = 0.06899999999999999

问题的原因

JavaScript 里的数字是采用 IEEE 754 标准的 64 位双精度浮点数。该规范定义了浮点数的格式,对于 64 位的浮点数在内存中的表示,最高的 1 位是符号位,接着的 11 位是指数,剩下的 52 位为有效数字,具体:

  • 第 0 位:符号位, s 表示 ,0 表示正数,1 表示负数;

  • 第 1 位到第 11 位:储存指数部分, e 表示 ;

  • 第 12 位到第 63 位:储存小数部分(即有效数字),f 表示,

符号位决定了一个数的正负,指数部分决定了数值的大小,小数部分决定了数值的精度。 IEEE 754 规定,有效数字第一位默认总是 1,不保存在 64 位浮点数之中。也就是说,有效数字总是 1.xx…xx 的形式,其中 xx..xx 的部分保存在 64 位浮点数之中,最长可能为 52 位。因此,JavaScript 提供的有效数字最长为 53 个二进制位(64 位浮点的后 52 位 + 有效数字第一位的 1)。

计算过程:

比如在 JavaScript 中计算 0.1 + 0.2 时,到底发生了什么呢?

首先,十进制的 0.1 和 0.2 都会被转换成二进制,但由于浮点数用二进制表达时是无穷的,例如。

0.1 -> 0.0001100110011001...(无限)
0.2 -> 0.0011001100110011...(无限)

IEEE 754 标准的 64 位双精度浮点数的小数部分最多支持 53 位二进制位,所以两者相加之后得到二进制为:

0.0100110011001100110011001100110011001100110011001100

因浮点数小数位的限制而截断的二进制数字,再转换为十进制,就成了 0.30000000000000004。所以在进行算术计算时会产生误差。

解决方案一

在计算不多的情况下, 我没有引入第三方库,这里我优先选择自己编写 ts 组件并导出

const MathCalc = {
  /**
   ** 加法函数,用来得到精确的加法结果
   ** 说明:javascript 的加法结果会有误差,在两个浮点数相加的时候会比较明显。这个函数返回较为精确的加法结果。
   ** 调用:sum(arg1,arg2)
   ** 返回值:arg1 加上 arg2 的精确结果
   **/
  sum(arg1: number, arg2: number) {
    let r1, r2
    try {r1 = arg1.toString().split('.')[1].length
    } catch (e) {r1 = 0}
    try {r2 = arg2.toString().split('.')[1].length
    } catch (e) {r2 = 0}
    const c = Math.abs(r1 - r2)
    const m = Math.pow(10, Math.max(r1, r2))
    if (c > 0) {const cm = Math.pow(10, c)
      if (r1 > r2) {arg1 = Number(arg1.toString().replace('.', ''))
        arg2 = Number(arg2.toString().replace('.', '')) * cm
      } else {arg1 = Number(arg1.toString().replace('.', '')) * cm
        arg2 = Number(arg2.toString().replace('.', ''))
      }
    } else {arg1 = Number(arg1.toString().replace('.', ''))
      arg2 = Number(arg2.toString().replace('.', ''))
    }
    return (arg1 + arg2) / m
  },
  /**
   ** 减法函数,用来得到精确的减法结果
   ** 说明:javascript 的减法结果会有误差,在两个浮点 g2)会比较明显。这个函数返回较为精确的减法结果。
   ** 调用:sub(arg1,arg2)
   ** 返回值:arg1 加上 arg2 的精确结果
   **/
  sub(arg1: number, arg2: number) {
    let r1, r2
    try {r1 = arg1.toString().split('.')[1].length
    } catch (e) {r1 = 0}
    try {r2 = arg2.toString().split('.')[1].length
    } catch (e) {r2 = 0}
    const c = Math.abs(r1 - r2)
    const m = Math.pow(10, Math.max(r1, r2))
    if (c > 0) {const cm = Math.pow(10, c)
      if (r1 > r2) {arg1 = Number(arg1.toString().replace('.', ''))
        arg2 = Number(arg2.toString().replace('.', '')) * cm
      } else {arg1 = Number(arg1.toString().replace('.', '')) * cm
        arg2 = Number(arg2.toString().replace('.', ''))
      }
    } else {arg1 = Number(arg1.toString().replace('.', ''))
      arg2 = Number(arg2.toString().replace('.', ''))
    }
    return (arg1 - arg2) / m
  },
  /**
   ** 乘法函数,用来得到精确的乘法结果
   ** 说明:javascript 的乘法结果会有误差,在两个浮点数相乘的时候会比较明显。这个函数返回较为精确的乘法结果。
   ** 调用:mul(arg1,arg2)
   ** 返回值:arg1 乘以 arg2 的精确结果
   **/
  mul(arg1: number, arg2: number) {
    let m = 0
    const s1 = arg1.toString()const s2 = arg2.toString()
    try {m += s1.split('.')[1].length
    } catch (e) {console.log('🚀85 行 e ➡️', e)
    }
    try {m += s2.split('.')[1].length
    } catch (e) {console.log('🚀90 行 e ➡️', e)
    }
    return ((Number(s1.replace('.', '')) * Number(s2.replace('.',''))) /
      Math.pow(10, m)
    ) },
  /**
   ** 除法函数,用来得到精确的除法结果
   ** 说明:javascript 的除法结果会有误差,在两个浮点数相除的时候会比较明显。这个函数返回较为精确的除法结果。
   ** 调用:div(arg1,arg2)
   ** 返回值:arg1 除以 arg2 的精确结果
   **/
  div(arg1: number, arg2: number) {
    let t1 = 0,
      t2 = 0
    try {t1 = arg1.toString().split('.')[1].length
    } catch (e) {console.log('🚀109 行 e ➡️', e)
    }
    try {t2 = arg2.toString().split('.')[1].length
    } catch (e) {console.log('🚀114 行 e ➡️', e)
    }
    const r1 = Number(arg1.toString().replace('.', ''))
    const r2 = Number(arg2.toString().replace('.', ''))
    return (r1 / r2) * Math.pow(10, t2 - t1)
  },
}
export default MathCalc

测试

// 导入组件
import MathCalc from '@/components/Math/index'
// 加法
const a = MathCalc.sum(0.1, 0.2)
console.log('🚀 0.1+0.2 ➡️', 0.1 + 0.2)
console.log('🚀 a ➡️', a)
console.log('🚀 a===0.3 ➡️', a === 0.3)
// 减法
const b = MathCalc.sub(0.3, 0.2)
console.log('🚀 0.3-0.2 ➡️', 0.3 - 0.2)
console.log('🚀 b ➡️', b)
console.log('🚀 b===0.1 ➡️', b === 0.1)
// 乘法
const c = MathCalc.mul(0.1, 0.2)
console.log('🚀 0.1*0.2 ➡️', 0.1 * 0.2)
console.log('🚀 c ➡️', c)
console.log('🚀 c===0.02 ➡️', c === 0.02)
// 除法
const e = MathCalc.div(0.3, 0.1)
console.log('🚀 0.3/0.1 ➡️', 0.3 / 0.1)
console.log('🚀 e ➡️', e)
console.log('🚀 e===3 ➡️', e === 3.0)

打印结果

🚀 0.1+0.2 ➡️ 0.30000000000000004
🚀 a ➡️ 0.3
🚀 a===0.3 ➡️ true
🚀 0.3-0.2 ➡️ 0.09999999999999998
🚀 b ➡️ 0.1
🚀 b===0.1 ➡️ true
🚀 0.1*0.2 ➡️ 0.020000000000000004
🚀 c ➡️ 0.02
🚀 c===0.02 ➡️ true
🚀 0.3/0.1 ➡️ 2.9999999999999996
🚀 e ➡️ 3
🚀 e===3 ➡️ true

解决方案二

通常这种对精度要求高的计算都应该交给后端去计算和存储,因为后端有成熟的库来解决这种计算问题。前端也有几个不错的类库:

Math.js

Math.js 是专门为 JavaScript 和 Node.js 提供的一个广泛的数学库。它具有灵活的表达式解析器,支持符号计算,配有大量内置函数和常量,并提供集成解决方案来处理不同的数据类型
像数字,大数字 (超出安全数的数字),复数,分数,单位和矩阵。 功能强大,易于使用。

官网:http://mathjs.org/

GitHub:GitHub - josdejong/mathjs: An extensive math library for JavaScript and Node.js

decimal.js

为 JavaScript 提供十进制类型的任意精度数值。

官网:decimal.js API

GitHub:GitHub - MikeMcl/decimal.js: An arbitrary-precision Decimal type for JavaScript

big.js

官网:big.js API

GitHub:GitHub - MikeMcl/big.js: A small, fast JavaScript library for arbitrary-precision decimal arithmetic.

这几个类库帮我们解决很多这类问题,不过通常我们前端做这类运算通常只用于表现层,应用并不是很多。所以很多时候,一个函数能解决的问题不需要引用一个类库来解决。

解决方案三

toFixed() 方法

toFixed() 方法使用定点表示法来格式化一个数,会对结果进行四舍五入。语法为:

number.toFixed(digits)

参数 digits 表示小数点后数字的个数;介于 0 到 20 (包括)之间,实现环境可能支持更大范围。如果忽略该参数,则默认为 0。


文章作者: 张登友
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 张登友 !
  目录