外观
为什么 0.1 + 0.2 不等于 0.3?
JavaScript 的数字是 IEEE 754 双精度浮点。0.1 和 0.2 不能用有限的二进制精确表示,相加的结果比 0.3 多一个很小的误差。显示和比较前要按精度处理,金额不要用二进制浮点做账。
原理
0.1 + 0.2 === 0.3 为假。误差是表示问题,不是加法写错了。Number.EPSILON 大约是 2.22e-16,可以用来判断两个浮点数是否足够接近:
js
function nearlyEqual(a, b) {
return Math.abs(a - b) < Number.EPSILON
}这只适合接近 1 的量级。大数要按相对误差比,或者先按业务规定的小数位 Math.round(n * 100) / 100。
整数在 Number.MAX_SAFE_INTEGER(2^53 - 1)以内是精确的。超过这个范围用 BigInt,但 BigInt 不能和 Number 直接混算,也不能表示小数。
金额用「分」这样的整数,或者用十进制库。接口如果把金额当字符串返回,前端不要立刻丢进浮点加法。
工程里怎么处理
账目和标识分开处理。金额要精确到「分」这种业务单位;订单号、雪花 id 只是标识,不参与加减。展示时再格式化,格式化的结果不要拿回去继续算。
金额在接口和数据库里用整数。 单位是分、厘,或业务约定的最小单位。1999 表示 19.99 元。加减用整数,天然精确。比例运算先乘后除,并规定舍入:
js
function taxCents(amountCents, rateBps) {
return Math.round((amountCents * rateBps) / 10000)
}
taxCents(1010, 1300) // 10.10 元按 13% 计税,结果是 131 分rateBps 是万分比,1300 表示 13%。这样全程没有二进制小数。
用户输入是字符串时,按十进制拆开,不要先 Number 再乘 100。 输入框限制两位小数后,"19.99" 可以直接拆成整数。
js
function yuanToCents(input) {
const text = String(input).trim()
const negative = text.startsWith("-")
const [intPart, frac = ""] = text.replace("-", "").split(".")
const cents = Number(intPart) * 100 + Number((frac + "00").slice(0, 2))
return negative ? -cents : cents
}
yuanToCents("19.99") // 1999
yuanToCents("0.10") // 10第三位及以后的数字这里直接丢掉。真实收银要按产品规定的舍入(常见是四舍五入到分)写进这一步,而不是依赖 toFixed。
已有的浮点结果只用于兼容旧数据。 Math.round(n * 100) 能应付多数两位小数,但 1.005 * 100 这类值在变成 Number 时已经偏了。新接口改为整数字段;旧接口如果仍是 19.99 这种 JSON 数字,前端用 decimal 库转成整数再算,不要继续 +。
税率、折扣、汇率用十进制库。 规则比「乘一个整数再四舍五入」复杂时,用 decimal.js、big.js 或 bignumber.js。库内部用十进制,最后再按业务精度 toFixed 或 toDecimalPlaces 收成整数分。页面上不要到处 new Decimal,收成一个金额模块:入参出参都是「分」或十进制字符串。
显示放在边界上。 计算结束后再格式化。Intl.NumberFormat 负责千分位和货币符号,它不参与运算。
js
function formatYuan(cents) {
return new Intl.NumberFormat("zh-CN", {
style: "currency",
currency: "CNY"
}).format(cents / 100)
}
formatYuan(1999) // "¥19.99"这里的除法只为了显示。比较两笔金额时比较分,不要比较格式化后的字符串,也不要比较两个浮点元。
长整型 id 保持字符串。 超过 2^53 - 1 的 id,接口用字符串。如果后端 JSON 里仍是数字,用支持长整数的解析器,或在解析前用正则把该字段收成字符串。前端不把这种 id 做加减,列表的 key 也用字符串。
追问
toFixed 能当精确运算吗?
它返回的是舍入后的字符串,用来展示。连续运算仍要在数字或整数上进行,不能靠反复 toFixed 再转回来保证全流程精确。
大整数 id 为什么会变?
超过安全整数的 id 放进 JSON.parse 会丢精度。让接口用字符串传 id,或在解析时把长整型字段当成字符串。
易错点
Number.EPSILON不能直接拿来比较任意大小的数。BigInt没有小数,和Number混合运算会抛错。(0.1 + 0.2).toFixed(1)看起来对了,不代表中间值是精确的0.3。- 先把金额变成浮点再
* 100取整,误差会留在「分」上。输入和接口能给字符串或整数时,不要绕这一步。 - 展示用的
¥19.99再解析回数字继续相加,会把格式化误差带回账目。