文章目录
问题本质回顾:为什么 0.1 + 0.2 !== 0.3
JavaScript 使用 IEEE 754 双精度浮点数标准(64位) 表示所有数字:
- 符号位:1 bit
- 指数位:11 bits
- 尾数(有效数字):52 bits(实际精度约 53 位,隐含前导 1)
1. 进制转换导致无限循环
十进制小数 0.1 和 0.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) / 10n → 0n // 因为 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.js 或 big.js |
| 学习目的 | 用字符串模拟加法(如 LeetCode 415 扩展) |
| 避免踩坑 | 永远不要相信 0.1 + 0.2 === 0.3 |
717

被折叠的 条评论
为什么被折叠?



