JavaScript 中的精度丢失与分摊不平问题及解决方案(2026最新版) 💰
大家好!这是很多前端(尤其是金融、电商、支付、报表)开发者最头疼的问题:
console.log(0.1 + 0.2); // 0.30000000000000004
console.log(0.3 - 0.2); // 0.09999999999999998
console.log(19.9 * 100); // 1989.9999999999998
console.log(1.005.toFixed(2)); // "1.00" 而不是 "1.01"
更可怕的是分摊不平(金额拆分时总和对不上):
假设 100 元分给 3 个人,每人应得 33.333… 元
用 toFixed(2) 后:33.33 + 33.33 + 33.33 = 99.99(差1分!)
今天这篇保姆级+生产级文章,一次性彻底讲透原因 + 所有解决方案(从简单到企业级),看完你再也不会被精度坑了。
1. 根本原因:IEEE 754 双精度浮点数(必懂!)
JavaScript 的 Number 类型底层是 64位 IEEE 754 双精度浮点数:
- 1位符号 + 11位指数 + 52位尾数(有效精度约15-16位十进制)
- 二进制无法精确表示大部分十进制小数(就像十进制无法精确表示1/3)
经典二进制转换演示(0.1 的二进制是无限循环):
大整数精度丢失(超过 Number.MAX_SAFE_INTEGER = 9007199254740991):
console.log(9007199254740992 === 9007199254740993); // true!已丢失
2. 常见场景 & 问题汇总
| 场景 | 典型表现 | 后果 |
|---|---|---|
| 加减乘除小数 | 0.1+0.2、0.3-0.2 | 显示多位小数或不相等 |
| toFixed() | 1.005.toFixed(2) → “1.00” | 银行级四舍五入错误 |
| 大ID(雪花算法) | JSON.parse后后几位变0 | ID重复、数据错乱 |
| 金额分摊 | 100 / 3 = 33.333… → toFixed后总和少1分 | 分摊不平(最致命) |
| 循环累加 | 0.1 加 10 次 ≠ 1 | 报表总计对不上 |
3. 解决方案对比(2026推荐排序)
方案1:最简单——转为整数计算(推荐日常金额)
把元转为分(或更小单位),全部用整数运算,最后再除。
// 金额用分存储
const price = 1999; // 19.99元
const qty = 3;
const total = price * qty; // 5997 分
console.log((total / 100).toFixed(2)); // "59.97" 完美
分摊不平终极解决(银行家舍入 + 尾差调整):
function distribute(amount, parts) {
const base = Math.floor(amount / parts);
const remainder = amount % parts;
const result = new Array(parts).fill(base);
for (let i = 0; i < remainder; i++) result[i]++;
return result.map(v => v / 100); // 转回元
}
distribute(10000, 3); // [3334, 3333, 3333] 分 → 总和正好100.00元
方案2:原生 BigInt(整数超大数专用,2026最推荐)
const a = 9007199254740992n;
const b = 1n;
console.log(a + b); // 9007199254740993n 精确!
金额场景(推荐结合方案1):
const priceInFen = 1999n;
const total = priceInFen * 3n; // 5997n
console.log(Number(total) / 100); // 59.97
方案3:高精度库(金融/科学计算必备)
2026年主流库对比(生产验证):
推荐优先级:
- decimal.js(最推荐)—— 任意精度、API最友好
- bignumber.js —— 轻量、速度快
- big.js —— 极简
- Dinero.js / currency.js —— 专门做货币
decimal.js 实战代码(复制即用):
import Decimal from 'decimal.js';
Decimal.set({ precision: 20, rounding: Decimal.ROUND_HALF_EVEN });
const a = new Decimal('0.1');
const b = new Decimal('0.2');
console.log(a.plus(b).toString()); // "0.3" 完美
const total = new Decimal('100');
const perPerson = total.dividedBy(3).toFixed(2); // "33.33"
console.log(perPerson); // 不会分摊不平
toFixed 增强版(解决1.005问题):
function accurateToFixed(num, digits = 2) {
return new Decimal(num).toFixed(digits);
}
accurateToFixed(1.005); // "1.01"
方案4:其他辅助技巧
- JSON大数:用
json-bigint或BigInt序列化 - 比较时用
Number.EPSILON:Math.abs(a - b) < Number.EPSILON - 显示时永远用
.toFixed(2)+ 库
4. 生产最佳实践(强烈建议)
- 所有金额后端存“分”或字符串,前端用
Decimal或整数 - 前端计算全部走库,绝不用原生
+ - * / - 分摊场景必须用整数分摊 + 尾差调整算法
- 大ID一律用
BigInt或字符串 - 测试:写单元测试覆盖
0.1+0.2、1.005.toFixed、100/3
5. 小结 & 推荐工具
一句话记住:
永远不要相信 JS 原生 Number 做精确计算,尤其是钱!
立即行动:
- 金额类项目 → 安装
decimal.js+ 改成整数分存储 - 大数ID → 用
BigInt或json-bigint - 快速修复 → 先用
toFixed(2)+Number.EPSILON兜底
想看完整 decimal.js 金额工具类(含分摊、税费、汇率、格式化)?
或者 BigInt + 分的完整金融系统示例?
直接评论回复 “1” 或 “2”,我下一篇文章立刻发完整可运行代码 + GitHub仓库!
把这篇收藏转发给你的团队吧 —— 精度问题一旦上线,就是真金白银的bug!
我们下一期继续深挖更多JS底层坑~ 🚀
(本文所有示例基于2026年Chrome 134 / Node 22 实测,库版本均为最新稳定版)