Sunday面试指南

为什么 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); // 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")); // 30n

TypeScript 这里使用 BigInt,Python 使用整数,两边都没有先把金额转成二进制小数。真实接口还需要限制输入长度和金额范围;BigInt 也不能直接按普通 JSON 数值发送,需要约定字符串等传输形式。

如果业务有利息、汇率或复杂小数运算,就不能只靠“全部保留两位”。应选择十进制计算方案,并明确精度和舍入规则。

从原始文本解析整数金额及拒绝未定义舍入

面试官继续追问

所有整数都能被 Number 精确表示吗?

不是。连续整数的安全范围有上限,超出后相邻整数可能被表示成同一个值。大 ID 应按字符串或合适的整数类型处理,不能只看它没有小数点就放心。

给结果加一个 EPSILON,再 round,可以解决所有舍入问题吗?

不能。数字量级、负数、输入误差和目标舍入规则都会影响结果。某个用例过了,不等于建立了通用的十进制计算方案。

Decimal 直接接收已经计算过的 float,可以消掉之前的误差吗?

不能倒回去恢复原始十进制意图。要精确保留输入,就从原始十进制字符串等可靠表示进入计算,别等浮点运算结束才换类型。

面试速记卡

  • 原因:有限的二进制浮点,不能精确表示所有十进制小数。
  • 显示:格式化负责输出文字,不修复之前的计算。
  • 比较:容差来自任务与量级,不固定套用 EPSILON。
  • 金额:从原始十进制表示进入整数或十进制计算。
  • 边界:安全整数范围、舍入规则和传输类型都要说明。

公司面试真题

这道题暂未收录可核验的公司真题来源。你可以先阅读本文解析,或浏览已收录的公司面试真题。

浏览公司面试真题 →
简历汪永久免费在线制作简历,模板直接套用、导出无水印,永久免费、下载免费,不需要付费解锁任何功能。去写简历