解决浮点计算精准度丢失的问题

192 阅读3分钟
0.1 + 0.2 // 0.30000000000000004

0.1 + 0.3 // 0.4

0.1 + 0.4 // 0.5

0.1 + 0.2 为什么不等于 0.3

我们知道,js 中所有的值在计算机底层都是以二进制来进行存储的。

js 使用 Number 类型表示数字(整数和浮点数),遵循 IEEE-754 标准,通过 64 位二进制来表示一个数字。

我们打开 IEEE-754标准数字到二进制转换工具,输入 0.1,点击 Not Rounded,可以看到 0.1 在计算机底层存储的 64 为二进制字符。

// 符号位 + 指数位 + 有效数字部分(不包含前面的1.1)
0.1 // '0011111110111001100110011001100110011001100110011001100110011010'

0.2 // '0011111111001001100110011001100110011001100110011001100110011010'

而在 Js 中,我们平时写的数字,浏览器都认为是十进制的,那浏览器肯定有一个操作,把我们写的 10 进制值转换为二进制。

如果我们来实现一个十进制转二进制呢,也就是 n.toString(2)

二进制转十进制进制

整数部分,除2取余,逆序排列 不断除2,拿余数倒序拼接得二进制,拿 12 举例,如下图

// 整数(兼容了负数) 十进制转二进制
let decimal2Binary = function (decimal) {
  let isNegative = decimal < 0; // 是否是负数
  
  decimal = Math.abs(decimal); // 都变成正数计算
  
  let binary = [decimal % 2]; // 存二进制字符 
  let integer = Math.floor(decimal / 2);

  while(integer) {
    binary.unshift(integer % 2); // 取余数
    integer = Math.floor(integer / 2); // 向下取整 0.5 -> 0 直到 为 0
  }

  return isNegative ? `-${ binary.join('') }` : binary.join('');
}

decimal2Binary(12) // 1100
decimal2Binary(-12) // -1100

小数部分,乘2取整,顺序排列 不断乘以2,取整数部分,余数继续乘以2,继续取整数部分,直到取整后余数为0。

我们得到的二进制数是 0.0011001100110011..,这样无尽的的循环,但是计算进底层最多能存储 64 位,所以会对十进制 0.1 转成的二进制进行截取,这样就没法保证精度了。

结论:为什么不等

浮点数在计算机底层存储的时候,十进制的 0.1 转化为存储的二进制值,可能被舍掉一部分「因为最多只有 64 位」,所以本身和原来的十进制就不一样了,这是所有编程语言都存在的问题,因为浮点数在后端语言中,也是按二进制进行存储的。 而 0.1 转二进制就被截取了一部分, 所以计算机底层进行的运算 0.1 + 0.2 本身就是不准确的,最后转成浏览器能识别的十进制,这样也可能是一个不准确的很长的值,例如可能是这样的 0.300000000000000040000... 但是浏览器也会存在长度的限制(小数点后17位),会截掉一部分,而最后面全是 0 的省略掉。

0.1 + 0.2 // 0.30000000000000004  
0.1 + 0.3 // 0.4 -> 其实是 0.4000000000000000000000001222222xxx 截取17位小数时候发现后面全是0 故返回 0.4

0.3 + 0.6 // 0.8999999999999999 后面也是截取掉的

所以导致浮点数计算不精确的原因有两个

  1. 十进制的浮点数转二进制时候,面临的计算机只截取64位二进制值的问题。
  2. 二进制浮点数运算结果转十进制时,面临的浏览器只展示小数点后17位问题。

怎么解决浮点数计算不精确的问题(这里示范加法)

1.乘以一定系数,避免浮点数运算,变成整数,运算结果再除以系数。

// 根据小数点后位数 获取合适系数
const getCoefficient = function(num) {
  // 数字 -> 字符串 -> 小数点位数
  num = num + '';

  let [,char = ''] = num.split('.');

  return Math.pow(10, char.length);
} 

// 加法
const plus = function plus(num1, num2) {
  num1 = +num1;
  num2 = +num2;

  if (isNaN(num1) || isNaN(num2)) return NaN;

  // 最大系数
  let coefficient = Math.max(getCoefficient(num1), getCoefficient(num2));

  return (num1 * coefficient + num2 * coefficient) / coefficient;
}