JavaScript 精度丢失问题

代码极光
• 阅读 8775
// 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

// 2. 两数相减
// 1.5 - 1.2 = 0.30000000000000004
// 0.3 - 0.2 = 0.09999999999999998

// 3. 两数相乘
// 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

// 4. 不一样的数却相等
// 1000000000000000128 === 1000000000000000129

计算机的底层实现就无法完全精确表示一个无限循环的数,而且能够存储的位数也是有限制的,所以在计算过程中只能舍去多余的部分,得到一个尽可能接近真实值的数字表示,于是造成了这种计算误差。

比如在 JavaScript 中计算0.1 + 0.2时,十进制的0.1和0.2都会被转换成二进制,但二进制并不能完全精确表示转换结果,因为结果是无限循环的。

// 百度进制转换工具
0.1 -> 0.0001100110011001...
0.2 -> 0.0011001100110011...

In JavaScript, Number is a numeric data type in the double-precision 64-bit floating point format (IEEE 754). In other programming languages different numeric types can exist, for examples: Integers, Floats, Doubles, or Bignums.

根据 MDN这段关于Number的描述 可以得知,JavaScript 里的数字是采用 IEEE 754 标准的 64 位双精度浮点数。该规范定义了浮点数的格式,最大最小范围,以及超过范围的舍入方式等规范。所以只要不超过这个范围,就不会存在舍去,也就不会存在精度问题了。比如:

// Number.MAX_SAFE_INTEGER 是 JavaScript 里能表示的最大的数了,超出了这个范围就不能保证计算的准确性了
var num = Number.MAX_SAFE_INTEGER;
num + 1 === num +2 // = true

实际工作中我们也用不到这么大的数或者是很小的数,也应该尽量把这种对精度要求高的计算交给后端去计算,因为后端有成熟的库来解决这个计算问题。前端虽然也有类似的库,但是前端引入一个这样的库代价太大了。

排除直接使用的数太大或太小超出范围,出现这种问题的情况基本是浮点数的小数部分在转成二进制时丢失了精度,所以我们可以将小数部分也转换成整数后再计算。网上很多帖子贴出的解决方案就是这种:

var num1 = 0.1
var num2 = 0.2
(num1 * 10 + num2 * 10) / 10 // = 0.3

但是这样转换整数的方式也是一种浮点数计算,在转换的过程中就可能存在精度问题,比如:

1306377.64 * 10 // = 13063776.399999999
1306377.64 * 100 // = 130637763.99999999
var num1 = 2.22;
var num2 = 0.1;
(num1 * 10 + num2 * 10) / 10 // = 2.3200000000000003

所以不要直接通过计算将小数转换成整数,我们可以通过字符串操作,移动小数点的位置来转换成整数,最后再同样通过字符串操作转换回小数:

/**
 * 通过字符串操作将一个数放大或缩小指定倍数
 * @num 被转换的数
 * @m   放大或缩小的倍数,为正表示小数点向右移动,表示放大;为负反之
 */
function numScale(num, m) {
  // 拆分整数、小数部分
  var parts = num.toString().split('.');
  // 原始值的整数位数
  const integerLen = parts[0].length;
  // 原始值的小数位数
  const decimalLen = parts[1] ? parts[1].length : 0;
  
  // 放大,当放大的倍数比原来的小数位大时,需要在数字后面补零
  if (m > 0) {
    // 补多少个零:m - 原始值的小数位数
    let zeros = m - decimalLen;
    while (zeros > 0) {
      zeros -= 1;
      parts.push(0);
    }
  // 缩小,当缩小的倍数比原来的整数位大时,需要在数字前面补零
  } else {
    // 补多少个零:m - 原始值的整数位数
    let zeros = Math.abs(m) - integerLen;
    while (zeros > 0) {
      zeros -= 1;
      parts.unshift(0);
    }
  }

  // 小数点位置,也是整数的位数: 
  //    放大:原始值的整数位数 + 放大的倍数
  //    缩小:原始值的整数位数 - 缩小的倍数
  var index = integerLen + m;
  // 将每一位都拆到数组里,方便插入小数点
  parts = parts.join('').split('');
  // 当为缩小时,因为可能会补零,所以使用原始值的整数位数
  // 计算出的小数点位置可能为负,这个负数应该正好是补零的
  // 个数,所以小数点位置应该为 0
  parts.splice(index > 0 ? index : 0, 0, '.');

  return parseFloat(parts.join(''));
}
/**
 * 获取小数位数
 */
function getExponent(num) {
  return Math.floor(num) === num ?
    0 : num.toString().split('.')[1].length;
}

/**
 * 两数相加
 */
function accAdd(num1, num2) {
  const multiple = Math.max(getExponent(num1), getExponent(num2));
  return numScale(numScale(num1, multiple) + numScale(num2, multiple), multiple * -1);
}

测试用例:

describe('accAdd', function() {
  it('(0.1, 0.2) = 0.3', function() {
    assert.strictEqual(0.3, _.accAdd(0.1, 0.2))
  })
  it('(2.22, 0.1) = 2.32', function() {
    assert.strictEqual(2.32, _.accAdd(2.22, 0.1))
  })
  it('(11, 11) = 22', function() {
    assert.strictEqual(22, _.accAdd(11, 11))
  })
})
点赞
收藏
评论区
推荐文章
blmius blmius
4年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
美凌格栋栋酱 美凌格栋栋酱
7个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
Karen110 Karen110
4年前
一篇文章带你了解JavaScript日期
日期对象允许您使用日期(年、月、日、小时、分钟、秒和毫秒)。一、JavaScript的日期格式一个JavaScript日期可以写为一个字符串:ThuFeb02201909:59:51GMT0800(中国标准时间)或者是一个数字:1486000791164写数字的日期,指定的毫秒数自1970年1月1日00:00:00到现在。1\.显示日期使用
待兔 待兔
1年前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Wesley13 Wesley13
3年前
Java:利用BigDecimal类巧妙处理Double类型精度丢失
本篇要点简单描述浮点数十进制转二进制精度丢失的原因。介绍几种创建BigDecimal方式的区别。整理了高精度计算的工具类。学习了阿里巴巴Java开发手册关于BigDecimal比较相等的规定。经典问题:浮点数精度丢失精度丢失的问题是在其他计算机语言中也都会出
Wesley13 Wesley13
3年前
JS中有趣的知识
1.分号与换行functionfn1(){return{name:'javascript'};}functionfn2(){return{name:'javascript'
Stella981 Stella981
3年前
Js四则运算精度问题处理
JavaScript小数在做四则运算时,精度会丢失,这会在项目中引起诸多不便。先看个具体的例子://较小的数运算console.log(0.099999990.00000001);//0.09999999999999999console.log(0.099999990.00000001);//0.09
Wesley13 Wesley13
3年前
35岁是技术人的天花板吗?
35岁是技术人的天花板吗?我非常不认同“35岁现象”,人类没有那么脆弱,人类的智力不会说是35岁之后就停止发展,更不是说35岁之后就没有机会了。马云35岁还在教书,任正非35岁还在工厂上班。为什么技术人员到35岁就应该退役了呢?所以35岁根本就不是一个问题,我今年已经37岁了,我发现我才刚刚找到自己的节奏,刚刚上路。
Wesley13 Wesley13
3年前
oracle小数点前零丢失的问题
1.问题起源 oracle数据库字段值为小于1的小数时,使用char类型处理,会丢失小数点前面的0 例如0.35就变成了.35 2.解决办法:(1)用to\_char函数格式化数字显示 select    to\_char(0.338,'fm9999999990.00')fromdual; 结果:0.34 
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这