外观
0.1 + 0.2 为什么不等于 0.3
约 1067 字
0.1 + 0.2 为什么不等于 0.3
每个 JS 开发者都遇到过:
0.1 + 0.2 === 0.3 // false
0.1 + 0.2 // 0.30000000000000004不是 JS 的 bug,是 IEEE 754 双精度浮点数的本来面目。 不只 JS:Java、Python、C 的 float/double 都一样。 这篇讲清为什么,以及金额计算怎么不死人。
一、根因:二进制存不了所有小数
计算机用二进制。十进制小数转二进制:
0.5 → 0.1 (二进制,精确)
0.25 → 0.01 (精确)
0.1 → 0.00011001100110011… (无限循环!)0.1 在二进制里是无限循环小数,跟十进制里 1/3 = 0.333… 一样。 float64 只有 64 位(52 位存小数部分),存不下无限位 —— 只能截断:
0.1 实际存的是 0.1000000000000000055511151231257827021181583404541015625「存的时候偏一点,算的时候误差就露出来了」:
0.1 + 0.2 // 两个近似值相加 = 0.30000000000000004(又一个近似值)跟 1/3 + 1/3 + 1/3 ≈ 1(其实 0.999…)一个道理, 只是十进制误差我们肉眼能忍,二进制误差在界面上很刺眼。
二、其他翻车现场
0.3 - 0.1 === 0.2 // false(0.19999999999999998)
1.005 * 100 // 100.49999999999999(不是 100.5)
(0.07 * 100).toFixed(2) // '7.00' 但 (0.07*100) 是 7.000000000000001
0.1.toFixed(20) // '0.10000000000000000555'还有个大数问题:
9007199254740992 + 1 === 9007199254740992 // true!
// 2^53 之后的整数也存不准(安全整数上限 Number.MAX_SAFE_INTEGER)雪花 ID、微博 ID 超 2^53 就丢精度 —— 详见 JSON 指南(在新窗口打开)。
三、为什么显示 0.30000000000000004 不直接显示 0.3
JS 的 Number → String 用「最短能唯一表示该浮点值的十进制串」。 0.30000000000000004 是能区分于 0.3 的最短表示 —— 显示引擎很努力了,但底层值确实不是 0.3。
四、怎么办:分场景
场景一:比较小数(别用 ===)
// ❌
if (a + b === 0.3)
// ✅ 容差比较
const EPSILON = 1e-10
Math.abs((0.1 + 0.2) - 0.3) < EPSILON // true
// ✅ 或 Number.EPSILON 思路(按值大小缩放)
function almostEqual(a, b, epsilon = Number.EPSILON * Math.pow(2, 3)) {
return Math.abs(a - b) < epsilon
}场景二:展示小数(toFixed 的坑)
(1.005).toFixed(2) // '1.00' 不是 '1.01'!(1.005 实际存的是 1.00499…)toFixed 对临界值不可靠。要精确控制用库(见下), 或者先 round:
// 更稳的两位小数(也有边界问题,重大场合用库)
(Math.round(1.005 * 100) / 100).toFixed(2)场景三:金额计算(唯一正解)
永远不要用 float 算钱。
// ❌ 0.1 + 0.2 的幽灵在账户里游荡
let price = 19.99
let count = 3
price * count // 59.970000000000006正解 A:整数运算。金额全部按「分」存:
const priceInCents = 1999 // 19.99 元 → 1999 分
const total = priceInCents * 3 // 5997 分(整数,精确)
const yuan = (total / 100).toFixed(2) // '59.97'(只在展示时除)加减乘都精确;除法要小心(不能整除时按业务规则 rounding)。
正解 B:十进制库:
// decimal.js / big.js / dinero.js
const price = new Decimal('19.99')
price.times(3).toString() // '59.97'字符串构造 + 十进制运算,彻底绕开二进制。 后端同理:Java 用 BigDecimal、Python 用 decimal.Decimal、 MySQL 用 DECIMAL 列(见 MySQL 基础(在新窗口打开))。
口诀:存储用整数(分),计算用整数或 Decimal,只在展示时变小数。
五、什么时候不用管
不是所有场景都要如临大敌:
- 不需要精确的:动画插值、物理模拟、统计图表 —— 误差无所谓;
- 整数的场景:数组长度、ID —— 只要不超 2^53 没问题;
- 一次性展示:
(Math.random() * 100).toFixed(1)—— 无所谓。
红线只有一条:钱、库存数量、优惠券面值 —— 这种错一分 就是事故的场景,必须整数/Decimal。
六、语言对照
JS Number(float64)+ BigInt(整数)+ 各种 decimal 库
Python float(同样问题)+ Decimal + fractions.Fraction
Java double(同样问题)+ BigDecimal
MySQL FLOAT/DOUBLE(同样问题)+ DECIMAL(精确)
Go float64(同样问题)+ shopspring/decimal所有「图灵完备」的语言都有这问题 —— 因为都跑在二进制硬件上。 别骂语言,骂 IEEE 754(也别真骂,它是 40 年前工程上的最优折中)。
小结
浮点三句话:二进制存不全小数、0.1+0.2 是近似值相加、 显示 shortest-string 已经尽力。应对三件套: 比较用容差、展示用库或整数、算钱用「分」。 下次面试官问「0.1+0.2 为什么」,把这套讲出来 —— 从原理到实战,就是一篇「靠谱程序员」的答卷。