浮点数精度问题

解释 JavaScript 浮点数为什么会出现精度误差,以及常见的处理方式。

#type / concept #status / growing #tech / dev / frontend

浮点数精度问题

一句话定义

JavaScript 使用 IEEE 754 双精度浮点数存储所有 Number 类型,部分十进制小数无法精确表示为二进制小数,导致运算出现精度误差。

核心机制

IEEE 754 双精度结构

一个 64 位浮点数由三部分组成:

  • 1 位符号位(sign)
  • 11 位指数位(exponent)
  • 52 位尾数位(mantissa)
| S | EEEEEEEEEEE | MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM |
  1       11                        52

为什么 0.1 + 0.2 !== 0.3

十进制的 0.1 转成二进制是无限循环小数:

0.0001100110011001100110011...(1100 循环)

双精度只能存 52 位尾数,截断后产生微小误差。0.1 和 0.2 各自有微小偏差,相加后偏差累积:

0.1 + 0.2
// 0.30000000000000004

0.1 + 0.2 === 0.3
// false

toFixed / toPrecision 的边界

// toFixed 返回字符串,有四舍五入
(1.005).toFixed(2)   // "1.00" 而不是 "1.01"
// 因为 1.005 的实际存储值略小于 1.005

// toPrecision 指定有效数字位数
(0.1 + 0.2).toPrecision(1)   // "0.3"
(0.1 + 0.2).toPrecision(17)  // "0.30000000000000004"

toFixedtoPrecision 都不能从根本上解决精度问题,只是在显示层面做四舍五入。

常见处理方式

1. 放大为整数运算

// 金额计算:放大到分
const a = 0.1 * 100  // 10
const b = 0.2 * 100  // 20
(a + b) / 100        // 0.3

2. Number.EPSILON 做误差判断

function equal(a, b) {
  return Math.abs(a - b) < Number.EPSILON
}

equal(0.1 + 0.2, 0.3)  // true

注意:Number.EPSILON 是 2^-52,约 2.2e-16。对于放大后的整数相减,EPSILON 可能过小。

3. toFixed 取指定位数后转回数字

parseFloat((0.1 + 0.2).toFixed(10))  // 0.3

这是精度够用场景下的快速方案,但本质上是截断而非精确计算。

4. BigInt 处理整数大数

const a = 9007199254740993n  // 超过 Number.MAX_SAFE_INTEGER
const b = 1n
a + b  // 9007199254740994n

BigInt 只能处理整数,不能直接用于小数运算。金额场景可以全程用”分”的 BigInt 表示。

5. 专用库

  • decimal.js / big.js / bignumber.js:任意精度小数运算
  • 金融、科学计算等对精度要求严格的场景推荐使用

边界与常见误解

  • Number.EPSILON 不是”最小精度差”,而是 1 与大于 1 的最小浮点数之差。它只适合判断接近 1 量级的数是否”足够相等”。
  • toFixed 返回字符串,不返回数字。parseFloat 转回来时可能再次引入误差。
  • Math.round 也不能解决浮点问题:Math.round(1.005 * 100) / 100 得到 1 而不是 1.01。
  • BigInt 不能与 Number 混合运算,也不能表示小数。
  • 0.1 + 0.2 === 0.3 在几乎所有语言中都是 false,这不是 JS 的 bug,而是 IEEE 754 的通用行为。
创建于 2026/4/9 更新于 2026/5/27