在javascript中0.1+0.2 !== 0.3 ,为什么?如何才能让其相等?

问题本质回顾:为什么 0.1 + 0.2 !== 0.3

JavaScript 使用 IEEE 754 双精度浮点数标准(64位) 表示所有数字:

  • 符号位:1 bit
  • 指数位:11 bits
  • 尾数(有效数字):52 bits(实际精度约 53 位,隐含前导 1)

1. 进制转换导致无限循环

十进制小数 0.10.2 转成二进制是无限循环小数

0.1₁₀ = 0.00011001100110011...₂ (无限循环)
0.2₁₀ = 0.0011001100110011...₂ (无限循环)

由于 IEEE 754 只能存储有限位(约 53 位有效二进制位),所以必须截断或舍入,这就造成了精度丢失

2. 对阶与尾数移位加剧误差

浮点加法需要“对阶”——将指数小的数右移尾数以对齐指数。右移时低位会被舍去(0舍1入),进一步引入误差。

最终结果:

0.1 + 0.2 === 0.30000000000000004

解决办法

1.转为整数(大数)运算

function add(a, b) {
  const maxLen = Math.max(
    a.toString().split(".")[1].length,
    b.toString().split(".")[1].length
  );
  const base = 10 ** maxLen;
  const bigA = BigInt(base * a);
  const bigB = BigInt(base * b);
  const bigRes = (bigA + bigB) / BigInt(base);
  return Number(bigRes);
}

这个方法真的可行吗,我们来验证一下
在这里插入图片描述
很明显,小数位数多的时候,这个方法不行,555.1283744747477+88.9282873746647=644.0566618494124

问题出在哪里?
错误 1:base * a 仍然是浮点运算!

关键问题就在这里:

const bigA = BigInt(base * a); // 例如:BigInt(10 * 0.1) → BigInt(1) ✅
// 但如果是 0.1 → 10^17 呢?或者 0.1111111111111111?

⚠️ base * a先做浮点乘法,然后再转 BigInt
这意味着:在转成大整数之前,已经发生了精度丢失!

👉 举例:

0.1 * 100 // → 10.000000000000001?不!
// 实际上 JS 中 0.1 * 10 是 1,因为某些情况下能“碰巧”正确
// 但 0.1 * 10000000000000000 → 1000000000000000.1?错!

更严重的是,0.1 本身无法精确表示,所以 10 ** 17 * 0.1 很可能不是整数!

错误 2:BigInt 不能处理小数,且 / 是整除!
(1n + 2n) / 10n0n  // 因为 BigInt 的除法是整除(向下取整)

所以即使你前面都对了,最后 / BigInt(base) 也会丢掉小数部分


正确做法:如何安全地“转为整数运算”?

要避免浮点误差,必须从字符串入手,而不是从 Number 开始!

正确方法:从字符串提取小数位,避免浮点参与
function add(a, b) {
  // 转为字符串,避免浮点误差
  const [aInt = "", aDec = ""] = a.toString().split('.');
  const [bInt = "", bDec = ""] = b.toString().split('.');

  // 统一小数位数(补零)
  const maxDecLen = Math.max(aDec.length, bDec.length);
  const paddedA = aInt + aDec.padEnd(maxDecLen, '0');
  const paddedB = bInt + bDec.padEnd(maxDecLen, '0');

  // 转为 BigInt(此时是整数表示)
  const bigA = BigInt(paddedA);
  const bigB = BigInt(paddedB);
  const sum = bigA + bigB;

  // 插入小数点
  const resultStr = sum.toString();
  const intPart = resultStr.slice(0, -maxDecLen) || "0";
  const decPart = resultStr.slice(-maxDecLen).padStart(maxDecLen, '0');

  return parseFloat(intPart + "." + decPart);
}

// 测试
console.log(add(0.1, 0.2)); // 0.3 ✅
console.log(add(0.1, 0.2) === 0.3); // true ✅

✅ 这才是“转为整数运算”的正确姿势全程避免浮点运算,从字符串构造整数


使用 Number.EPSILON 的正确方式

你写的这个函数有个小 bug:

function isEqual(a, b) {
  return Math.abs(a - b) < Number.EPSILON;
}
问题:Number.EPSILON 是相对误差,不能直接用于任意大小的数!

Number.EPSILON ≈ 2.22e-16,这是1附近的最小可表示差值。对于接近 0 的数还行,但对于大数(如 1e10),这个阈值太小了。

正确写法:使用相对误差
function isEqual(a, b) {
  if (a === b) return true;
  const diff = Math.abs(a - b);
  return diff < Number.EPSILON * Math.max(Math.abs(a), Math.abs(b));
}

console.log(isEqual(0.1 + 0.2, 0.3)); // true ✅

或者更稳健的版本(考虑 0 的情况):

function isEqual(a, b) {
  return Math.abs(a - b) < Number.EPSILON * Math.max(1, Math.abs(a), Math.abs(b));
}

2.字符串模拟加法(高精度)

function addStrings(num1, num2) {
  let i = num1.length - 1, j = num2.length - 1;
  let carry = 0;
  let result = '';

  while (i >= 0 || j >= 0 || carry) {
    const digit1 = i >= 0 ? parseInt(num1[i--]) : 0;
    const digit2 = j >= 0 ? parseInt(num2[j--]) : 0;
    const sum = digit1 + digit2 + carry;
    result = (sum % 10) + result;
    carry = Math.floor(sum / 10);
  }

  return result;
}

function addDecimals(a, b) {
  const [int1 = "", dec1 = ""] = a.toString().split('.');
  const [int2 = "", dec2 = ""] = b.toString().split('.');
  
  const maxDec = Math.max(dec1.length, dec2.length);
  const paddedDec1 = dec1.padEnd(maxDec, '0');
  const paddedDec2 = dec2.padEnd(maxDec, '0');

  const intSum = addStrings(int1, int2);
  const decSum = addStrings(paddedDec1, paddedDec2);

  // 处理小数进位
  if (decSum.length > maxDec) {
    const carry = decSum[0];
    const newDec = decSum.slice(1);
    const newInt = addStrings(intSum, carry);
    return newInt + '.' + newDec.padStart(maxDec - 1, '0');
  } else {
    return intSum + '.' + decSum.padStart(maxDec, '0');
  }
}

console.log(addDecimals(0.1, 0.2)); // "0.3"

我们再来验证一下
在这里插入图片描述

3.使用高精度库

不要重复造轮子!生产环境推荐:

npm install decimal.js
const Decimal = require('decimal.js');

const a = new Decimal(0.1);
const b = new Decimal(0.2);
const sum = a.plus(b);

console.log(sum.equals(0.3)); // true
console.log(sum.toString());  // "0.3"

decimal.js 内部使用字符串或整数表示数字,完全避开 IEEE 754 问题。

4.最佳实践建议

场景推荐方案
一般精度比较使用 Math.abs(a - b) < Number.EPSILON * Math.max(...)
金融、高精度计算使用 decimal.jsbig.js
学习目的用字符串模拟加法(如 LeetCode 415 扩展)
避免踩坑永远不要相信 0.1 + 0.2 === 0.3
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Sherry Tian

打赏1元鼓励作者

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值