这是一个经典的 JavaScript 浮点数精度问题,也是所有使用 IEEE 754 标准的编程语言中的普遍现象。
为什么 0.1 + 0.2 !== 0.3?
1. 二进制表示问题
计算机使用二进制(base-2)表示数字,而人类使用十进制(base-10)。
在二进制中:
0.1(十进制)是一个无限循环二进制小数:0.00011001100110011...0.2(十进制)也是一个无限循环二进制小数:0.0011001100110011...
就像 1/3 在十进制中是 0.333333... 一样,某些十进制小数在二进制中无法精确表示。
2. IEEE 754 双精度浮点数
JavaScript 使用 64 位(双精度)表示浮点数:
- 1 位符号位
- 11 位指数位
- 52 位尾数位
由于 52 位尾数的限制,无限循环的二进制小数会被截断,导致精度丢失。
3. 计算过程中的累积误差
// 实际上计算机是这样计算的:
0.1 ≈ 0.1000000000000000055511151231257827021181583404541015625
0.2 ≈ 0.200000000000000011102230246251565404236316680908203125
0.1 + 0.2 ≈ 0.3000000000000000444089209850062616169452667236328125
0.3 ≈ 0.299999999999999988897769753748434595763683319091796875
如何让它们相等?
方法 1:设置误差范围(最常用)
// 使用 Number.EPSILON(ES6+)
function numbersEqual(a, b) {
return Math.abs(a - b) < Number.EPSILON;
}
console.log(numbersEqual(0.1 + 0.2, 0.3)); // true
// 自定义误差范围
const areEqual = (a, b, epsilon = 0.000001) => Math.abs(a - b) < epsilon;
方法 2:转换为整数计算
// 将浮点数转换为整数进行计算
function addFloat(a, b) {
const precision = Math.pow(10, 10); // 10位精度
return (Math.round(a * precision) + Math.round(b * precision)) / precision;
}
console.log(addFloat(0.1, 0.2) === 0.3); // true
方法 3:使用 toFixed 和 parseFloat
// 四舍五入到指定位数
const result = parseFloat((0.1 + 0.2).toFixed(10));
console.log(result === 0.3); // true
// 注意:toFixed 返回的是字符串
方法 4:使用第三方库
// 使用 decimal.js
import { Decimal } from 'decimal.js';
const sum = new Decimal(0.1).plus(0.2);
console.log(sum.equals(0.3)); // true
// 使用 big.js
import Big from 'big.js';
const result = new Big(0.1).plus(0.2);
console.log(result.eq(0.3)); // true
方法 5:ES6+ 的 Number.isInteger 检查
// 对于整数检查
Number.isInteger(0.1 + 0.2); // false
Number.isInteger(0.3); // false
// 更好的方法:先转换为整数再比较
function areFloatsEqual(a, b) {
const factor = 10 ** Math.max(
a.toString().split('.')[1]?.length || 0,
b.toString().split('.')[1]?.length || 0
);
return Math.round(a * factor) === Math.round(b * factor);
}
实际应用建议
场景 1:金额计算(最重要)
// 永远不要用浮点数表示金额(单位:分)
const price = 10; // 表示 10元,实际存储 1000分
const quantity = 3;
const total = price * quantity; // 3000分 = 30元
// 或者使用专门的库
场景 2:UI 显示
// 显示时格式化
const displayPrice = (priceInCents / 100).toFixed(2);
场景 3:比较浮点数
// 通用比较函数
function compareFloats(a, b, precision = 0.000001) {
if (a === b) return true; // 快速检查
// 处理 NaN
if (isNaN(a) && isNaN(b)) return true;
// 处理 Infinity
if (!isFinite(a) || !isFinite(b)) return a === b;
// 比较相对误差
const diff = Math.abs(a - b);
const max = Math.max(Math.abs(a), Math.abs(b));
return diff <= Math.max(precision, max * Number.EPSILON);
}
最佳实践总结
- 金融计算:使用整数(以分为单位)或专门的库(如 decimal.js)
- 一般计算:使用
toFixed()或设置误差范围 - 比较时:永远不要直接比较浮点数,使用误差范围
- 理解本质:这不是 JavaScript 的 Bug,而是计算机科学的普遍现象
有趣的测试
// 更多例子
console.log(0.1 + 0.7 === 0.8); // false
console.log(0.3 + 0.6 === 0.9); // false
console.log(1.5 + 1.5 === 3); // true(因为 1.5 可以精确表示)
// 解决方案对比
console.log((0.1 * 10 + 0.2 * 10) / 10 === 0.3); // true
console.log(Math.round((0.1 + 0.2) * 1e10) / 1e10 === 0.3); // true
理解这个问题的关键在于认识到:计算机是二进制的,而我们是十进制的,这种基数差异导致了某些数字无法精确表示。