为什么 JavaScript 的 0.1 + 0.2 不等于 0.3?精度问题怎么处理?
下面是一段教学用的模拟面试。
🧑💻 面试官:0.1 + 0.2 为什么不等于 0.3?
🙋♂️ 我:浮点数精度问题,可以用 toFixed 保留小数。
🧑💻 面试官:toFixed 返回字符串。格式化之后,之前计算的结果就变准确了吗?
🙋♂️ 我:没有,它主要影响显示。
🧑💻 面试官:那把所有金额乘 100 再算,就一定没问题吗?Math.round(1.005 * 100) 为什么又可能不是期待的结果?
先决定你需要的是「近似计算、好看的显示,还是精确的十进制结果」,再选择处理方式。
面试速答(60 秒版)
JavaScript 的 Number 使用二进制浮点数表示数字。像 0.1、0.2 这样的十进制小数,转成二进制以后不能有限地写完,只能保存接近的值。相加以后再舍入,结果就可能和保存下来的 0.3 不完全相同。
处理时要看用途。界面显示可以格式化;近似计算的比较,可以按数值大小和实际误差要求设置容差;金额等要求精确十进制语义的计算,更适合从十进制字符串转成最小单位整数,或者使用可靠的十进制计算方案。
但 Number 的整数也有安全范围,不能无限放大。toFixed 不会修复之前的运算,Number.EPSILON 也不是能用于所有数值的固定误差标准。

知识点详解:数字写在代码里,存进内存后是什么样?
0.1 很短,为什么电脑却要近似保存?
在十进制里,1/3 需要写成 0.3333……,有限的小数位只能保存一个近似值。
二进制也有类似情况,只是“写不完”的数字换了。0.5 可以写成二进制的 0.1,因为它就是二分之一;十进制的 0.1 则需要不断延伸。
Number 的存储位数是有限的,因此只能挑一个能表示、又接近它的值。这里不是 JavaScript 特有的加法故障,常见二进制浮点实现都会遇到类似现象。MDN Number说明了 Number 的表示与安全整数范围。
console.log(0.1 + 0.2); // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3); // false
console.log(0.5 + 0.25 === 0.75); // trueprint(0.1 + 0.2) # 0.30000000000000004
print(0.1 + 0.2 == 0.3) # False
print(0.5 + 0.25 == 0.75) # True这里的 Python 示例对应常见 CPython 的 float。例子帮助咱们看到,同样是小数,并不是每次相加都产生可见误差。
计算结果和显示结果,要分开判断
假设页面只是展示测量结果,保留两位小数就足够。可以把结果格式化成 0.30,读者不会看到后面一长串数字。
但格式化做的是“按某种规则输出文字”。原来保存的浮点值还在,之前累计的误差也不会因为显示成两位小数自动消失。
如果后面还要计算,就应保留合适的计算数据;如果只是展示,就在输出边界格式化。别把每一步都转成字符串,再转回 Number,以为这样就建立了精确计算规则。

比较近似结果时,容差从哪里来?
假设计算的是坐标或测量值,你关心两个结果是否足够接近,而不是二进制表示是否完全一致。
这时可以比较差值,但阈值应该来自允许的误差,以及数字所在的量级。Number.EPSILON 文档特别提醒,数值越大,能表示的相邻值间距也会变化。
例如“差值小于 EPSILON”对接近 1 的某些演示有意义,却不是所有大数字的通用判断。实际计算可以结合绝对容差与相对容差,并根据任务要求选定它们。
金额对账则不能拿一个宽松容差随便判定“差不多”。如果账目允许的最小单位是一分,就应当明确按分计算、怎么舍入以及在哪一步舍入。
乘 100 之前,输入已经变成了什么?
假设接口给了字符串 “12.30”,业务约定最多两位小数。咱们可以直接按字符串中的整数部分和小数部分,得到 1230 分。
但如果先把任意输入转成 Number,再乘 100,浮点近似已经发生了。1.005 * 100 这样的表达式,就可能落在预期舍入位置的另一侧。
下面的两个示例只处理非负、至多两位小数的金额字符串;超出约定直接拒绝,不悄悄替业务决定第三位怎么舍入。
function toCents(text: string): bigint {
if (text.trim() !== text || !/^\d+(?:\.\d{1,2})?$/.test(text)) {
throw new Error("金额必须是非负且至多两位小数");
}
const [whole, fraction = ""] = text.split(".");
return BigInt(whole) * 100n + BigInt(fraction.padEnd(2, "0"));
}
console.log(toCents("0.10") + toCents("0.20")); // 30nimport re
def to_cents(text: str) -> int:
if re.fullmatch(r"[0-9]+(?:\.[0-9]{1,2})?", text) is None:
raise ValueError("金额必须是非负且至多两位小数")
whole, _, fraction = text.partition(".")
return int(whole) * 100 + int(fraction.ljust(2, "0"))
print(to_cents("0.10") + to_cents("0.20")) # 30TypeScript 这里使用 BigInt,Python 使用整数,两边都没有先把金额转成二进制小数。真实接口还需要限制输入长度和金额范围;BigInt 也不能直接按普通 JSON 数值发送,需要约定字符串等传输形式。
如果业务有利息、汇率或复杂小数运算,就不能只靠“全部保留两位”。应选择十进制计算方案,并明确精度和舍入规则。

面试官继续追问
所有整数都能被 Number 精确表示吗?
不是。连续整数的安全范围有上限,超出后相邻整数可能被表示成同一个值。大 ID 应按字符串或合适的整数类型处理,不能只看它没有小数点就放心。
给结果加一个 EPSILON,再 round,可以解决所有舍入问题吗?
不能。数字量级、负数、输入误差和目标舍入规则都会影响结果。某个用例过了,不等于建立了通用的十进制计算方案。
Decimal 直接接收已经计算过的 float,可以消掉之前的误差吗?
不能倒回去恢复原始十进制意图。要精确保留输入,就从原始十进制字符串等可靠表示进入计算,别等浮点运算结束才换类型。
面试速记卡
- 原因:有限的二进制浮点,不能精确表示所有十进制小数。
- 显示:格式化负责输出文字,不修复之前的计算。
- 比较:容差来自任务与量级,不固定套用 EPSILON。
- 金额:从原始十进制表示进入整数或十进制计算。
- 边界:安全整数范围、舍入规则和传输类型都要说明。
公司面试真题
这道题暂未收录可核验的公司真题来源。你可以先阅读本文解析,或浏览已收录的公司面试真题。
浏览公司面试真题 →