一、为什么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
四、方案选型指南
- 简单场景:toFixed足够应付显示需求,但计算过程仍需用整数法
- 金融系统:必须使用decimal.js这类专业库
- 高频交易:整数运算性能最佳(比decimal.js快10倍以上)
- 现代环境:BigInt是未来方向,但注意浏览器兼容性
特别注意:
- 永远不要在比较时直接使用
==或===比较浮点数 - 数据库存储建议使用DECIMAL/NUMERIC类型
- 前后端传输数据时建议以字符串形式传递金额
// 安全比较示例
function floatEqual(a, b, epsilon = 1e-10) {
return Math.abs(a - b) < epsilon;
}
五、终极解决方案架构
对于企业级金融系统,推荐分层处理:
- 输入层:将金额统一转换为整数(分/厘为单位)
- 计算层:全程使用整数运算或decimal.js
- 存储层:数据库使用DECIMAL(20,6)等固定精度类型
- 输出层:按需格式化为带小数点的字符串
// 完整示例:贷款计算器
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);
}
}
记住:在金钱面前,任何微小的误差都是不可接受的。选择适合你业务场景的方案,让每一分钱都精确无误。
评论