端经典面试题:为什么 0.1 + 0.2 !== 0.3?

🧮 前端经典面试题:为什么 0.1 + 0.2 !== 0.3?

在 JavaScript 控制台中输入以下代码:

javascript 复制代码
console.log(0.1 + 0.2); // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3); // false

这一刻,很多初学者的世界观崩塌了:"难道计算机连小学数学都算不对吗?"

当然不是。这背后涉及计算机组成原理中浮点数的存储机制。今天,我们就来揭开这个"精度丢失"的神秘面纱。

📂 目录

  1. [🔍 现象:不仅仅是 JS](#🔍 现象:不仅仅是 JS)
  2. [💻 根源:十进制与二进制的"翻译"困境](#💻 根源:十进制与二进制的“翻译”困境)
  3. [📏 标准:IEEE 754 双精度浮点数](#📏 标准:IEEE 754 双精度浮点数)
  4. [🧩 过程:0.1 和 0.2 是如何变成 0.3000...4 的?](#🧩 过程:0.1 和 0.2 是如何变成 0.3000…4 的?)
  5. [🛠️ 解决方案:如何在工程中处理精度问题?](#🛠️ 解决方案:如何在工程中处理精度问题?)
  6. [💡 总结](#💡 总结)

1. 🔍 现象:不仅仅是 JS

首先你要知道,这不是 JavaScript 的 Bug,而是几乎所有遵循 IEEE 754 标准的编程语言的共同特性。

你可以尝试在 Python、Java、C++ 甚至计算器中运行:

python 复制代码
# Python
print(0.1 + 0.2) # 0.30000000000000004
java 复制代码
// Java
System.out.println(0.1 + 0.2); // 0.30000000000000004

结论:只要使用双精度浮点数(Double),就会遇到这个问题。


2. 💻 根源:十进制与二进制的"翻译"困境

计算机底层只认识 01 (二进制)。

当我们写 0.1 时,计算机需要把它转换成二进制存储。

❌ 整数转换(完美)

十进制整数转二进制通常很完美。

例如:十进制 5 -> 二进制 101。没有精度损失。

⚠️ 小数转换(无限循环)

十进制小数转二进制,采用的是 "乘 2 取整,顺序排列" 法。

让我们试着把 0.1 转换成二进制:

  1. 0.1 × 2 = 0.2 0.1 \times 2 = 0.2 0.1×2=0.2 -> 取整 0
  2. 0.2 × 2 = 0.4 0.2 \times 2 = 0.4 0.2×2=0.4 -> 取整 0
  3. 0.4 × 2 = 0.8 0.4 \times 2 = 0.8 0.4×2=0.8 -> 取整 0
  4. 0.8 × 2 = 1.6 0.8 \times 2 = 1.6 0.8×2=1.6 -> 取整 1
  5. 0.6 × 2 = 1.2 0.6 \times 2 = 1.2 0.6×2=1.2 -> 取整 1
  6. 0.2 × 2 = 0.4 0.2 \times 2 = 0.4 0.2×2=0.4 -> 取整 0 (注意:这里回到了步骤 2,开始循环!)

所以,0.1 的二进制表示是:
0.0001100110011001100110011... ( 0011 无限循环 ) 0.0001100110011001100110011... (0011 \text{ 无限循环}) 0.0001100110011001100110011...(0011 无限循环)

同理,0.2 的二进制也是无限循环的:
0.001100110011001100110011... ( 0011 无限循环 ) 0.001100110011001100110011... (0011 \text{ 无限循环}) 0.001100110011001100110011...(0011 无限循环)

核心问题

计算机的内存是有限的(通常是 64 位),它存不下无限循环的小数

因此,它必须截断 (舍入)。这一截断,就产生了精度丢失


3. 📏 标准:IEEE 754 双精度浮点数

JavaScript 中的 Number 类型遵循 IEEE 754 双精度浮点数标准 ,占用 64 位(8 字节)

这 64 位被分为三部分:

部分 位数 作用
符号位 (Sign) 1 bit 0 表示正,1 表示负
指数位 (Exponent) 11 bits 科学计数法的指数部分
尾数位 (Mantissa/Fraction) 52 bits 有效数字部分(精度所在)

关键点

  • 尾数只有 52 位
  • 当二进制小数超过 52 位时,超出的部分会被舍入(通常是"0 舍 1 入")。
  • 这就是误差产生的地方。

4. 🧩 过程:0.1 + 0.2 是如何变成 0.3000...4 的?

虽然手动计算 64 位二进制非常复杂,但我们可以简化理解这个过程:

  1. 存储阶段

    • 0.1 被转换为二进制并截断存储为近似值 A A A。
    • 0.2 被转换为二进制并截断存储为近似值 B B B。
    • 此时, A A A 和 B B B 都已经不等于真实的 0.1 和 0.2 了,存在微小误差。
  2. 运算阶段

    • 计算机执行 A + B A + B A+B。
    • 由于二进制对齐指数等操作,可能会产生更多的低位误差。
  3. 输出阶段

    • 将结果的二进制转换回十进制显示。
    • 最终结果变成了 0.30000000000000004

直观理解

就像你用只有两位小数的计算器算 1 / 3 + 1 / 3 1/3 + 1/3 1/3+1/3。
1 / 3 ≈ 0.33 1/3 \approx 0.33 1/3≈0.33
0.33 + 0.33 = 0.66 0.33 + 0.33 = 0.66 0.33+0.33=0.66

而真实结果是 0.666... 0.666... 0.666...

这里的 0.00000000000000004 就是那个被舍去的"尾巴"累积出来的误差。


5. 🛠️ 解决方案:如何在工程中处理精度问题?

既然知道了原理,我们在实际开发中(尤其是涉及金额计算时)该如何避免坑呢?

✅ 方案一:转为整数计算(推荐用于金额)

这是最常用、最稳妥的方法。将小数放大为整数进行运算,然后再缩小。

javascript 复制代码
// 错误做法
console.log(0.1 + 0.2); // 0.30000000000000004

// 正确做法:乘以 10 的 N 次方,转为整数运算
function add(num1, num2) {
  const base = 10; // 根据小数位数决定,这里假设最多1位小数
  return (num1 * base + num2 * base) / base;
}

console.log(add(0.1, 0.2)); // 0.3

注意 :对于更复杂的场景,建议使用专门的库,如 decimal.jsbig.js,它们能处理任意精度的计算。

✅ 方案二:使用 toFixed 格式化(仅用于展示)

如果你只需要展示结果,不关心内部逻辑,可以使用 toFixed
注意toFixed 返回的是字符串,且在不同浏览器下舍入行为可能略有差异(虽现代浏览器已统一)。

javascript 复制代码
const result = 0.1 + 0.2;
console.log(result.toFixed(1)); // "0.3"
console.log(Number(result.toFixed(1))); // 0.3 (转回数字)

✅ 方案三:设置误差范围(Epsilon)

在比较两个浮点数是否相等时,不要直接用 ===,而是判断它们的差值是否小于一个极小的数(机器精度)。

javascript 复制代码
function isEqual(num1, num2) {
  return Math.abs(num1 - num2) < Number.EPSILON;
}

console.log(isEqual(0.1 + 0.2, 0.3)); // true

Number.EPSILON 是 JavaScript 中能表示的最小差值,约为 2.22 × 10 − 16 2.22 \times 10^{-16} 2.22×10−16。

✅ 方案四:使用 BigInt 或专用库

对于极高精度的科学计算或金融级应用,直接使用 BigDecimal 类似的库(如 decimal.js)。

javascript 复制代码
import Decimal from "decimal.js";

const a = new Decimal(0.1);
const b = new Decimal(0.2);
console.log(a.plus(b).toString()); // "0.3"

💡 总结

关键点 说明
根本原因 十进制小数(如 0.1)在二进制中是无限循环
存储限制 IEEE 754 双精度只有 52 位尾数,必须截断
影响范围 所有遵循 IEEE 754 的语言(JS, Java, Python, C++ 等)
最佳实践 金额计算 务必转为整数 或使用专用库
比较技巧 使用 Math.abs(a - b) < EPSILON 代替 ===

🚀 博主寄语

计算机不会犯错,它只是太"直男"了------它只能处理有限的二进制位。

作为开发者,我们的职责就是理解它的局限性,并用工程化的手段去弥补它。

记住口诀

浮点运算有误差,

二进制里无尽头。

金额计算转整数,

比较大小用阈值。

第三方库来帮忙,

精准无误不用愁。

希望这篇文档能帮你彻底搞懂浮点数精度问题!如果有疑问,欢迎在评论区留言。👇

喜欢这篇文章吗?记得点赞、收藏、转发哦! ❤️

相关推荐
ZC跨境爬虫1 小时前
跟着 MDN 学 HTML day_12:(HTML网页图片嵌入)
前端·javascript·css·ui·html
光影少年1 小时前
reeact虚拟DOM、Diff算法原理、key的作用与为什么不能用index
前端·react.js·掘金·金石计划
用户059540174461 小时前
大模型记忆存储踩坑实录:LangChain 的 ConversationBufferMemory 让我排查了 6 小时
前端·css
是上好佳佳佳呀2 小时前
【前端(十二)】JavaScript 函数与对象笔记
前端·javascript·笔记
你真的快乐吗2 小时前
@fuxishi/svg-icon:一个 Vue 3 svg本地图标+iconify图标组件库,让图标管理不再头疼
前端·vue.js·typescript
Rkgua2 小时前
ESModule和Commonjs模块的区别
前端·javascript
江南十四行2 小时前
ReAct Agent 基本理论与项目实战(二)
前端·react.js·前端框架
用户600071819102 小时前
【翻译】React 如何乱序流式输出 UI,却仍保持最终顺序
前端
江南十四行2 小时前
AI Agent应用类型及Function Calling开发实战(三)
服务器·前端·javascript