JavaScript 中的精度丢失与分摊不平问题及解决方案

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后后几位变0ID重复、数据错乱
金额分摊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年主流库对比(生产验证):

推荐优先级

  1. decimal.js(最推荐)—— 任意精度、API最友好
  2. bignumber.js —— 轻量、速度快
  3. big.js —— 极简
  4. 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-bigintBigInt 序列化
  • 比较时用 Number.EPSILONMath.abs(a - b) < Number.EPSILON
  • 显示时永远用 .toFixed(2) + 库

4. 生产最佳实践(强烈建议)

  1. 所有金额后端存“分”或字符串,前端用 Decimal 或整数
  2. 前端计算全部走库,绝不用原生 + - * /
  3. 分摊场景必须用整数分摊 + 尾差调整算法
  4. 大ID一律用 BigInt 或字符串
  5. 测试:写单元测试覆盖 0.1+0.21.005.toFixed100/3

5. 小结 & 推荐工具

一句话记住

永远不要相信 JS 原生 Number 做精确计算,尤其是钱!

立即行动

  • 金额类项目 → 安装 decimal.js + 改成整数分存储
  • 大数ID → 用 BigIntjson-bigint
  • 快速修复 → 先用 toFixed(2) + Number.EPSILON 兜底

想看完整 decimal.js 金额工具类(含分摊、税费、汇率、格式化)?
或者 BigInt + 分的完整金融系统示例

直接评论回复 “1”“2”,我下一篇文章立刻发完整可运行代码 + GitHub仓库!

把这篇收藏转发给你的团队吧 —— 精度问题一旦上线,就是真金白银的bug

我们下一期继续深挖更多JS底层坑~ 🚀

(本文所有示例基于2026年Chrome 134 / Node 22 实测,库版本均为最新稳定版)

文章已创建 4992

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

相关文章

开始在上面输入您的搜索词,然后按回车进行搜索。按ESC取消。

返回顶部