zoukankan      html  css  js  c++  java
  • 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

  • 相关阅读:
    OpenShift
    ant exec
    深入了解Ant构建工具 命令
    防止sql注入和跨站脚本攻击,跨站请求伪造以及一句话木马的学习记录
    Web攻防之XSS,CSRF,SQL注入(转)
    sublime text常用快捷键(转)
    fiddler使用心得记录
    python+tesseract验证码识别的一点小心得
    window脚本命令学习(转)
    python发送邮件(转)
  • 原文地址:https://www.cnblogs.com/shenjp/p/13901438.html
Copyright © 2011-2022 走看看