为什么0.1+0.2 ! == 0.3,如何让其相等

28 阅读3分钟

这是一个经典的 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.10.1000000000000000055511151231257827021181583404541015625
0.20.200000000000000011102230246251565404236316680908203125
0.1 + 0.20.3000000000000000444089209850062616169452667236328125
0.30.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);
}

最佳实践总结

  1. 金融计算:使用整数(以分为单位)或专门的库(如 decimal.js)
  2. 一般计算:使用 toFixed() 或设置误差范围
  3. 比较时:永远不要直接比较浮点数,使用误差范围
  4. 理解本质:这不是 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

理解这个问题的关键在于认识到:计算机是二进制的,而我们是十进制的,这种基数差异导致了某些数字无法精确表示