浮点数精度问题

1. 核心原因

JavaScript 中的数字类型(Number)遵循 IEEE 754 标准,使用 双精度 64 位浮点数 存储。

计算机内部使用 二进制 表示数字:

  • 0.1 (十进制) -> 0.0001100110011001100... (二进制)
  • 0.2 (十进制) -> 0.0011001100110011001100... (二进制)

这两个数字在二进制中都是 无限循环小数。由于存储位数是有限的(52 位尾数),必须进行截断,导致精度丢失。

相加后再转换回十进制,结果是一个极为接近 0.3 但不等于 0.3 的数: 0.30000000000000004

TIP

可借助该工具加深理解: https://devtool.tech/double-type

2. 如何解决?

(1) 使用 Number.EPSILON (比较)

Number.EPSILON 是 ES6 引入的一个极小常量,表示 1 与大于 1 的最小浮点数之间的差值。可以用来判断两个浮点数是否足够接近。

function areEqual(n1, n2) {
  return Math.abs(n1 - n2) < Number.EPSILON;
}

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

(2) 转换为整数计算

先将小数放大为整数(乘以 10^n),运算后再缩小回去。

function safeAdd(n1, n2) {
  const m = Math.pow(10, 10); // 放大倍数
  return Math.round((n1 * m) + (n2 * m)) / m;
}

console.log(safeAdd(0.1, 0.2)); // 0.3

(3) 使用第三方库

如果涉及金额计算,强烈建议不要手动计算,而是使用经过严格测试的库:

  • decimal.js
  • bignumber.js
import Decimal from 'decimal.js';
new Decimal(0.1).plus(0.2).toString(); // '0.3'

(4) 使用 toFixed

如果只是为了页面展示,可以直接使用 toFixed 保留小数位。注意它返回的是字符串。

(0.1 + 0.2).toFixed(1); // "0.3"

3. 大数危机 (BigInt)

除了小数精度,还有一个大数精度问题。也就是超过 Number.MAX_SAFE_INTEGER (2^53 - 1) 的整数会丢失精度。

解决方案:使用 BigInt (ES2020)。

const bigNum = 9007199254740991n;
const newNum = bigNum + 2n; // 9007199254740993n (精确)

4. 面试总结

  • 根本原因:IEEE 754 双精度浮点数存储导致的无限循环小数截断误差。
  • 解决方案:如果是比较,用 EPSILON;如果是计算,转整数处理或用库;如果是展示,用 toFixed