JS浮点运算精度问题

一、问题描述

在JS中,整数和浮点数都是Number数据类型,所有的数字都是以64位浮点数的形式存储的,即便是整数也是如此。所以我们在打印 1.00 的时候,显示的却是 1。当浮点数作为数学运算的时候,也会经常遇到一些奇怪的问题。比如下面:

// 加法运算
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
0.7 * 180 = 125.99999999999999
0.55 * 100 = 55.00000000000001

// 除法运算
0.3 / 0.1 = 2.9999999999999996
0.69 / 10 = 0.06899999999999999

这些问题常常会困扰我们的计算结果。出现很多意想不到的bug。那么问题的原因是什么呢?

二、问题原因

看了上面的结果,似乎有点不可思议,这种小学生都不会算错的题。强大的计算机怎么会出现错误呢。我们来看下具体的原因。

在js里,数字都是采用 IEEE 745标准 的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)。

三、计算过程

在JS中,计算 0.1 + 0.2 到底是个什么过程呢?

首先,十进制的 0.1 和 0.2 都会被转为二进制,但是浮点数使用二进制表达时是无穷的,比如:

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

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

0.0100110011001100110011001100110011001100110011001100 

因此再将上面的二进制转为十进制,就成了0.30000000000000004,所以浮点数在进行算术运算时会产生误差。

四、整数的精度问题

在JS中整数也同样存在精度问题,比如下面的代码

console.log(19571992547450991); //=> 19571992547450990
console.log(19571992547450991===19571992547450992); //=> true

原因和上面也相同,在js中,Number类型统一安装浮点数处理,整数是按照最大54位来算。

最大:2^53 - 1, Number.MAX_SAFE_INTEGER, 9007199254740991

最小:-(2^53 - 1),Number.MIN_SAFE_INTEGER,-9007199254740991

在这个范围之类的数,叫安全整数,所以超过这个方位,就会存在被社区的精度问题。

 

当然以上问题并不只是在JS中才会出现,几乎所有采用IEEE745标准的编程语言都会有精度问题,只不过其他很多语言都已经封装了方法来避免精度问题,而JS是一门弱类型的语言,从设计思想上就没有对浮点数有严格的数据类型,所以精度误差问题就会经常遇到。

五、解决方案

1. 类库

很多对精度要求较高的计算,都应该交给后端去计算和存储,因为后端有成熟的库来解决这种计算精度问题。当然前端也有一些不错的库:

Math.js

Math.js是专门为JS和node.js提供的一个数学库,它具有灵活的表达解析器,支持符号计算,配置有大量的函数和常量,比如大数计算,复数,分数,单位和矩阵,功能强大,易于使用

官网:https://mathjs.org/

gitHub: https://github.com/josdejong/mathjs

decimal.js

为JS提供十进制类型的任意精度的计算

gitHub: https://github.com/MikeMcl/decimal.js

big.js

gitHub: https://github.com/MikeMcl/big.js/

以上这些类库能够帮助我们解决很多问题,不过我们前端通常只做一些简单的加减乘除运算,使用库函数就显得有点多余,一个函数就能解决

2. 整数表示

对于整数,大数字可以采用字符串的形式进行表示

3. 格式化数字,金额,保留几位小数等

参考:https://www.html.cn/archives/7324

4. 浮点数运算

浮点运算的解决方案有很多,最常用的方法就是在判断浮点数运算结果钱对其结果进行精度缩小。

也可以采用一些网上封装好的函数进行处理:

加法运算

/**
 ** 加法函数,用来得到精确的加法结果
 ** 说明:javascript的加法结果会有误差,在两个浮点数相加的时候会比较明显。这个函数返回较为精确的加法结果。
 ** 调用:accAdd(arg1,arg2)
 ** 返回值:arg1加上arg2的精确结果
 **/
function accAdd (arg1, arg2) {
  var r1, r2, m, c;
  try {
    r1 = arg1.toString().split('.')[1].length;
  } catch (e) {
    r1 = 0;
  }
  try {
    r2 = arg2.toString().split('.')[1].length;
  } catch (e) {
    r2 = 0;
  }
  c = Math.abs(r1 - r2);
  m = Math.pow(10, Math.max(r1, r2));
  if (c > 0) {
    var 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;
}

// 给Number类型增加一个add方法,调用起来更加方便。
Number.prototype.add = function (arg) {
  return accAdd(arg, this);
};

减法运算

/**
 ** 减法函数,用来得到精确的减法结果
 ** 说明:javascript的减法结果会有误差,在两个浮点数相减的时候会比较明显。这个函数返回较为精确的减法结果。
 ** 调用:accSub(arg1,arg2)
 ** 返回值:arg1加上arg2的精确结果
 **/
function accSub (arg1, arg2) {
  var r1, r2, m, n;
  try {
    r1 = arg1.toString().split('.')[1].length;
  } catch (e) {
    r1 = 0;
  }
  try {
    r2 = arg2.toString().split('.')[1].length;
  } catch (e) {
    r2 = 0;
  }
  m = Math.pow(10, Math.max(r1, r2)); // last modify by deeka //动态控制精度长度
  n = (r1 >= r2) ? r1 : r2;
  return ((arg1 * m - arg2 * m) / m).toFixed(n);
}

// 给Number类型增加一个mul方法,调用起来更加方便。
Number.prototype.sub = function (arg) {
  return accMul(arg, this);
};

乘法运算

/**
 ** 乘法函数,用来得到精确的乘法结果
 ** 说明:javascript的乘法结果会有误差,在两个浮点数相乘的时候会比较明显。这个函数返回较为精确的乘法结果。
 ** 调用:accMul(arg1,arg2)
 ** 返回值:arg1乘以 arg2的精确结果
 **/
function accMul (arg1, arg2) {
  var m = 0;
  var s1 = arg1.toString();
  var s2 = arg2.toString();
  try {
    m += s1.split('.')[1].length;
  } catch (e) {
  }
  try {
    m += s2.split('.')[1].length;
  } catch (e) {
  }
  return Number(s1.replace('.', '')) * Number(s2.replace('.', '')) / Math.pow(10, m);
}

// 给Number类型增加一个mul方法,调用起来更加方便。
Number.prototype.mul = function (arg) {
  return accMul(arg, this);
};

除法运算

/**
 ** 除法函数,用来得到精确的除法结果
 ** 说明:javascript的除法结果会有误差,在两个浮点数相除的时候会比较明显。这个函数返回较为精确的除法结果。
 ** 调用:accDiv(arg1,arg2)
 ** 返回值:arg1除以arg2的精确结果
 **/
function accDiv (arg1, arg2) {
  var t1 = 0;
  var t2 = 0;
  var r1, r2;
  try {
    t1 = arg1.toString().split('.')[1].length;
  } catch (e) {
  }
  try {
    t2 = arg2.toString().split('.')[1].length;
  } catch (e) {
  }
  r1 = Number(arg1.toString().replace('.', ''));
  r2 = Number(arg2.toString().replace('.', ''));
  return (r1 / r2) * Math.pow(10, t2 - t1);
}

// 给Number类型增加一个div方法,调用起来更加方便。
Number.prototype.div = function (arg) {
  return accDiv(this, arg);
};

 

参考:

  https://juejin.im/post/6844903572979597319

  https://www.html.cn/archives/7340

 

posted on 2020-10-30 12:11  sjpqy  阅读(872)  评论(0编辑  收藏  举报

导航