你可能会觉得0.1 + 0.2 === 0.30000000000000004是一个程序错误,但实际上,它是IEEE 754双精度浮点数表示法在有限位宽下必然产生的系统性现象。这种精度误差会影响加减乘除、数值比较、格式化输出等几乎所有数字操作,但好消息是,通过针对性策略完全可以有效规避。

从大量线上业务日志和单元测试统计数据来看,以下三类浮点数精度误差出现得最为频繁,合计占比超过92%:
常见误差类型与发生频率
- 小数运算偏差:像
0.1 + 0.2这类组合,几乎100%会出现精度问题。类似0.1+0.3、0.2×0.3等运算,在金融或表单类项目中,每天都会触发数千次。 - toFixed 四舍五入失真:大约67%的
toFixed(n)调用,在边界值上会返回非预期结果。例如1.005.toFixed(2)得到"1.00",而不是预期的"1.01",根本原因在于底层存储的数值已经失真。 - 大整数截断:当数字超过
9007199254740992(即Number.MAX_SAFE_INTEGER + 1),约41%的ID、时间戳、计数器类字段会出现相邻值相等的情况,比如9007199254740992 === 9007199254740993。
按场景选择规避方式
不建议采用“一刀切”的方案,而应结合业务敏感度和性能要求来匹配最合适的策略:
- 金额/计费类计算:强制转换为整数单位(如“分”),全程使用
Math.round()防止误差放大。举个改造示例:(priceInYuan * 100 + feeInYuan * 100) / 100应改为(Math.round(priceInYuan * 100) + Math.round(feeInYuan * 100)) / 100。 - 高精度中间计算(如科学运算、坐标变换):引入
decimal.js或big.js,两者轻量且API兼容。建议尽量少用mathjs,除非你确实需要符号计算——它体积较大且启动较慢。 - 大整数 ID / 时间戳处理:后端返回字符串,前端用
BigInt解析(例如BigInt("9007199254740991000")),绝对禁止直接赋值给Number类型变量。 - 浮点数相等判断:禁用
===,改用容差比较:Math.abs(a - b) < 1e-10(金融场景建议1e-12)。
容易被忽略的关键细节
很多团队踩过坑,往往是因为忽略了这些底层事实:
parseFloat("0.1")本身已包含误差——它读取的是已失真的二进制近似值,并非原始的十进制0.1。Number('1.005').toFixed(2)返回"1.00",是因为1.005在内存中实际存储为1.0049999999999999,导致四舍五入向下。BigInt不能与Number混合运算(1n + 2会报错),必须显式转换,而且BigInt不支持小数。- 第三方库也非万能:
decimal.js默认精度为20位,如果需要更高精度(如加密货币场景),记得手动设置.precision(50)。
上线前必查清单
在部署包含数值计算的模块前,建议快速验证以下四项:
- 所有金额字段是否统一使用整数单位参与运算?
- 涉及
toFixed的地方,是否对输入值做了Math.round(value * 10**n) / 10**n预校正? - 大于
2^53的数字,是否以字符串形式接收并用BigInt处理? - 所有使用
===比较浮点数的地方,是否已替换为带epsilon容差的比较函数?
