【前端怪谈】为什么JS中0.1+0.2!==0.3?

235 阅读5分钟

前言

下面大家先来看一下这行诡异的代码,猜猜会输出什么

console.log(0.1 + 0.2);

在这里插入图片描述 是不是和所想的不太一样?下面我们来探索一下为什么它会这样吧。

一、问题原因

在计算机中数字无论是定点数还是浮点数都是以多位二进制的方式进行存储的。

tips:定点数浮点数是小数点在计算机中的两种表达方式。

在JS中数字采用的IEEE 754的双精度标准进行存储(存储一个数值所使用的二进制位数比较多,精度更准确)

大家可能在这会有一个疑问?IEEE 754双精度标准是什么?

image.png

IEEE 754规定,在计算机内部保存a时,默认这个数的第一位总是1,因此可以被舍去,只保存后面的xxxxxx部分。比如保存1.01的时候,只保存01,等到读取的时候,再把第一位的1加上去。这样做的目的,是节省1位有效数字。以64位浮点数为例,留给a只有52位,将第一位的1舍去以后,等于可以保存53位有效数字

那么我们是怎么处理舍去的数字的呢?我查阅了维基百科,得出了下面的答案:

  • 舍入到最接近:舍入到最接近,在一样接近的情况下偶数优先(Ties To Even,这是默认的舍入方式):会将结果舍入为最接近且可以表示的值,但是当存在两个数一样接近的时候,则取其中的偶数(在二进制中是以0结尾的)。
  • 朝+∞方向舍入:会将结果朝正无限大的方向舍入。
  • 朝-∞方向舍入:会将结果朝负无限大的方向舍入。
  • 朝0方向舍入:会将结果朝0的方向舍入。

第一种规则就是默认的舍入方式,可以理解为四舍五入,由于我们是使用二进制来存储的,所以这里应该成为0舍1入

前面我们知道两种计数方式都是以二进制的方式进行存储的,由于有效数也就是尾数只能表示53位精度,所以我们可以得知:

在此标准下,无法精确表示的非常大的整数将自动四舍五入。确切地说,JS 中的 Number类型只能安全地表示 -9007199254740991(-(2^53-1))9007199254740991(2^53-1)之间的整数,任何超出此范围的整数值都可能失去精度。

在定点数中,如果我们以8位二进制来存储数字。

对于整数来说,十进制的35会被存储为: 00100011 其代表 2^5 + 2^1 + 2^0。 对于纯小数来说,十进制的0.375会被存储为: 0.011 其代表 1/2^2 + 1/2^3 = 1/4 + 1/8 = 0.375

小数后二进制计算方法:

image.png

对于像0.1这样的数值用二进制表示你就会发现无法整除,最后算下来会是 0.000110011…由于存储空间有限,最后计算机会舍弃后面的数值,所以我们最后就只能得到一个近似值。

0.1和0.2在转换成二进制后会无限循环,由于标准位数的限制后面多余的位数会被截掉,此时就已经出现了精度的损失,相加后因浮点数小数位的限制而截断的二进制数字在转换为十进制就会变成0.30000000000000004

知道原因后,我们应该可以猜到,出问题的数字可能不光0.1+0.2。下列是总结出出现问题的式子:

  • 加法

0.1 + 0.2 = 0.30000000000000004

0.1 + 0.7 = 0.7999999999999999

0.2 + 0.4 = 0.6000000000000001

  • 减法

0.3 - 0.2 = 0.09999999999999998

1.5 - 1.2 = 0.30000000000000004

  • 乘法

0.8 * 3 = 2.4000000000000004

19.9 * 100 = 1989.9999999999998

  • 除法

0.3 / 0.1 = 2.9999999999999996

0.69 / 10 = 0.06899999999999999

二、解决办法

解决方法很简单,我们只要避免小数直接进行运算就够了,例如:

let a = 0.1;
let b = 0.2;
const add = (a, b) => {
  return (a * 10 + b * 10) / 10;
};
console.log(add(a, b));

我们先将两个数分别乘以10相加后再除以10就能得到结果啦。(这只是一道数学题哦)

三、大数相加

前面我们提到JS 中的 Number类型只能安全地表示 -9007199254740991(-(2^53-1))9007199254740991(2^53-1)之间的整数,如果我们需要使用比它大的数字该怎么办呢?

1、BigInt

BigInt是一种新的数据类型,用于当整数值大于 Number数据类型支持的范围时。这种数据类型允许我们安全地对大整数执行算术操作,表示高分辨率的时间戳,使用大整数id,等等。

let a = 321984671983710923709123709123709123720917312n;
let b = 2130192379012379102309123091230912309217309127309n;
console.log(a + b);

在这里插入图片描述

2、大数相加

虽然BigInt已经能够解决此类问题了,但是它的兼容性并不是很好 在这里插入图片描述 那我们也想大数相加怎么办呢?

既然没有轮子,我们就自己造一个!

function add(str1, str2) {
  //判断最大长度
  const len1 = str1.length;
  const len2 = str2.length;
  const len = Math.max(len1, len2);
  // 定义字符串
  let arr = "";
  // 进位数
  let w = 0;
  // 从最小位开始进行位运算
  for (let i = 0; i < len; i++) {
    // 根据位次取出相应数字
    let num1 = str1.charAt(len1 - i - 1);
    let num2 = str2.charAt(len2 - i - 1);
    // 两数相加
    let sum = Number(num1) + Number(num2);
    // 两数之和加上进位数
    sum = sum + w;
    // 重置进位数
    w = 0;
    // 对两数之和进行判断
    if (sum >= 10) {
      w++;
      sum = sum - 10;
    }
    arr = sum + arr;
    if (i === len - 1 && w === 1) {
      arr = w + arr;
    }
  }
  return arr;
}
console.log(add('709123907123170293012930790192321','79124309123091273093217092137091270217123121'));

在这里插入图片描述 大功告成!

总结

以上就是今天要讲的内容,前端怪谈将带你一起探索前端中的"怪事"。