如果你在JavaScript里计算过0.1 + 0.2,可能会惊讶地发现结果不是0.3,而是0.30000000000000004。这不是JavaScript的bug,而是所有使用IEEE 754标准的编程语言都会遇到的问题。简单来说,计算机用二进制表示小数时,就像我们用有限的位数表示无限循环小数一样,会产生精度丢失。
举个例子,0.1在二进制中是一个无限循环小数(0.00011001100110011...),计算机只能存储有限位数,所以会进行截断。当多个这样的近似值相加时,误差就会累积显现出来。我在电商项目中就遇到过这个问题:用户下单时,商品总价显示为199.99999999999997元而不是200元,虽然只差0.00000000000003,但用户看到后立刻打了客服电话投诉。
number-precision这个库的核心思路很巧妙:先把小数转换成整数进行计算,最后再转回小数。比如计算0.1 + 0.2时:
实际代码使用非常简单:
javascript复制import NP from 'number-precision'
console.log(NP.plus(0.1, 0.2)) // 输出0.3
我在金融项目里用它计算利息,对比了三种方案:
电商系统最怕价格计算出错。假设用户买了3件单价19.9元的商品,用原生JS计算:
javascript复制19.9 * 3 // 输出59.699999999999996
使用number-precision的正确姿势:
javascript复制NP.times(19.9, 3) // 输出59.7
更复杂的满减场景:
javascript复制// 商品总价300.5元,满300减50
const total = NP.plus(商品1, 商品2, 商品3)
const finalPrice = NP.minus(total, 50)
金融计算对精度要求极高,差一分钱都可能引发纠纷。假设计算7日年化收益率:
javascript复制// 错误做法
const dailyRate = 0.05 / 365 // 每日利率
const weekProfit = principal * dailyRate * 7 // 可能产生误差
// 正确做法
const dailyRate = NP.divide(0.05, 365)
const weekProfit = NP.times(NP.times(principal, dailyRate), 7)
做图表时,坐标轴刻度计算经常出问题:
javascript复制// 生成0到1之间10个等分点
const points = []
for(let i=0; i<=10; i++) {
points.push(NP.divide(i, 10)) // 用NP保证0.1,0.2...1.0完全均匀
}
游戏里的伤害计算公式通常很复杂:
javascript复制// 暴击伤害 = 攻击力 × (1 + 暴击倍率)
const critDamage = NP.times(attackPower, NP.plus(1, critRate))
// 实际伤害 = 暴击伤害 × (1 - 护甲减伤)
const realDamage = NP.times(critDamage, NP.minus(1, armorReduction))
虽然number-precision很好用,但在极端场景下要注意:
实测100万次计算耗时对比:
所以要根据场景选择方案。我在实际项目中建立了这样的规则:
当需要和Chart.js等库配合时,可以这样处理:
javascript复制// 先精确计算,再传入可视化库
const data = rawData.map(item => ({
x: NP.round(item.x, 2),
y: NP.round(item.y, 2)
}))
new Chart(ctx, { data })
银行家舍入法(四舍六入五成双)的实现:
javascript复制NP.round(0.535, 2) // 0.54
NP.round(0.525, 2) // 0.52
错误的写法:
javascript复制NP.times(NP.divide(a, b), c) // 可能产生中间误差
正确的写法:
javascript复制NP.times(a, c, 1/b) // 一次性计算
我在项目中总结的经验是:能用一次NP方法解决的问题,就不要嵌套多个NP调用。曾经因为嵌套调用导致过一个非常隐蔽的bug,花了整整两天才排查出来。