一、为什么0.1 + 0.2 ≠ 0.3?

让我们从一个经典问题开始:在控制台输入0.1 + 0.2,结果却是0.30000000000000004。这不是JavaScript的bug,而是所有采用IEEE 754标准的编程语言都会遇到的浮点数精度问题。

计算机用二进制存储小数时,就像用有限位数的十进制表示1/3(0.333...)一样,很多十进制小数在二进制中是无限循环的。例如:

// 技术栈:JavaScript
console.log(0.1.toString(2)); // "0.0001100110011001100110011001100110011001100110011001101"
console.log(0.2.toString(2)); // "0.001100110011001100110011001100110011001100110011001101"

这两个二进制数相加时,就像用有限位数计算1/3 + 2/3,结果可能不会正好等于1。

二、金融计算的致命陷阱

在电商、银行系统中,这种误差会被放大。假设计算商品折扣:

// 错误示范:直接使用浮点数计算
const price = 19.99;
const discount = 0.2;
const total = price * (1 - discount); // 实际得到15.992000000000002

// 银行利息计算更危险
const principal = 10000;
const rate = 0.025; // 年利率2.5%
const interest = principal * rate; // 可能得到250.00000000000003

这些微小的误差在批量处理百万级交易时,会导致严重的资金对账问题。

三、四大解决方案实战

方案1:toFixed + Number转换

// 技术栈:JavaScript
const fix = (num, precision = 2) => 
  Number(parseFloat(num).toFixed(precision));

console.log(fix(0.1 + 0.2)); // 0.3
console.log(fix(15.992000000000002)); // 15.99

注意:toFixed本身会四舍五入,可能不适合精确计算。

方案2:整数运算(推荐)

// 技术栈:JavaScript
function moneyCalc(amount, rate) {
  const factor = 100; // 保留2位小数
  return Math.round(amount * factor * rate) / factor;
}

console.log(moneyCalc(19.99, 0.8)); // 15.99(精确值)

方案3:第三方库(decimal.js)

// 技术栈:JavaScript + decimal.js
import Decimal from 'decimal.js';

const calcInterest = (principal, years, rate) => {
  return new Decimal(principal)
    .times(new Decimal(1).plus(rate))
    .pow(years)
    .toDecimalPlaces(2);
};

console.log(calcInterest(1000, 5, 0.03).toString()); // "1159.27"

方案4:BigInt(ES2020+)

// 技术栈:现代JavaScript
const preciseAdd = (a, b) => {
  const scale = 100n; // 相当于小数点后两位
  const bigA = BigInt(Math.round(a * 100));
  const bigB = BigInt(Math.round(b * 100));
  return Number(bigA + bigB) / 100;
};

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

四、方案选型指南

  1. 简单场景:toFixed足够应付显示需求,但计算过程仍需用整数法
  2. 金融系统:必须使用decimal.js这类专业库
  3. 高频交易:整数运算性能最佳(比decimal.js快10倍以上)
  4. 现代环境:BigInt是未来方向,但注意浏览器兼容性

特别注意:

  • 永远不要在比较时直接使用=====比较浮点数
  • 数据库存储建议使用DECIMAL/NUMERIC类型
  • 前后端传输数据时建议以字符串形式传递金额
// 安全比较示例
function floatEqual(a, b, epsilon = 1e-10) {
  return Math.abs(a - b) < epsilon;
}

五、终极解决方案架构

对于企业级金融系统,推荐分层处理:

  1. 输入层:将金额统一转换为整数(分/厘为单位)
  2. 计算层:全程使用整数运算或decimal.js
  3. 存储层:数据库使用DECIMAL(20,6)等固定精度类型
  4. 输出层:按需格式化为带小数点的字符串
// 完整示例:贷款计算器
class LoanCalculator {
  constructor() {
    this.Decimal = window.Decimal || Decimal;
  }

  calculate(principal, annualRate, months) {
    const rate = new this.Decimal(annualRate).div(12);
    const numerator = rate.times(new this.Decimal(principal));
    const denominator = new this.Decimal(1).minus(
      new this.Decimal(1).plus(rate).pow(-months)
    );
    return numerator.div(denominator).toDecimalPlaces(2);
  }
}

记住:在金钱面前,任何微小的误差都是不可接受的。选择适合你业务场景的方案,让每一分钱都精确无误。