我用 JavaScript 算钱,0.1 加 0.2 居然不等于 0.3,算出来的金额总是差那么一点点,我对着这串诡异的小数尾巴排查了大半天的深度复盘

我做金额计算,加加减减算总价,结果总带条诡异的小数尾巴:0.1+0.2 算出来是 0.30000000000000004、和 0.3 比较还返回 false,误差累积导致显示和对账总差一两分钱。我简直不敢信计算机连加法都算错。深究才懂这不是 JS 的 bug、而是几乎所有语言共有的浮点(IEEE 754)局限:计算机用二进制,而 0.1 换成二进制是无限循环小数(像十进制 1/3 除不尽),尾数位有限只能存近似值,两个近似值相加误差就累积成了带尾巴的数。这篇从浮点是近似的、二进制存不下 0.1 讲起,到算钱用整数最小单位(分)/精确小数库、浮点比较用 epsilon、大整数用 BigInt 的正解,以及那句最戳心的——计算机的抽象有物理局限、不是完美数学概念,精度敏感处要懂底层别无脑信任。

我用 JavaScript 算钱,0.1 加 0.2 居然不等于 0.3,算出来的金额总是差那么一点点,我对着这串诡异的小数尾巴排查了大半天的深度复盘

这是一个让我对"浮点数精度"刻骨铭心的故事。我在做一个涉及金额计算的功能:把几个商品的价格加起来、算个总价、做点折扣。逻辑再简单不过了——不就是加加减减、乘以个折扣率嘛。在我朴素的认知里,计算机算个数,那还能算错?

可结果,处处透着诡异:我把几个金额加起来,得到的总价,常常带着一条诡异的小数尾巴——比如,本该是 0.3 的,算出来却是 0.30000000000000004;本该是 0.3 的总价,我去和 0.3 比较是否相等,竟然返回 false!更要命的是,这些误差累积起来,导致显示给用户的金额、和数据库里对账的金额,总是差那么一两分钱——对于一个算钱的系统,这是致命的。我当时简直不敢相信:0.1 + 0.2,这么简单的算式,JavaScript 算出来,居然不等于 0.3?!计算机,连小学生都会的加法,还能算错?我反复测,console.log(0.1 + 0.2),赫然就是 0.30000000000000004。直到我去深究计算机如何存储小数,才恍然大悟,补上了关于浮点数最重要的一课:原来,问题不在 JavaScript,而是几乎所有语言都共有的、计算机表示小数的根本局限——浮点数(遵循 IEEE 754 标准),根本无法精确地表示大多数十进制小数!这要从计算机的存储说起:计算机里,一切都是二进制;而很多在十进制下很"整"的小数(比如 0.1、0.2),换算成二进制,却是"无限循环"的(就像十进制里,1/3 = 0.333... 除不尽一样,0.1 在二进制里,也是个无限循环小数)。可计算机存储一个数的位数是有限的,它存不下一个无限循环的小数,只能截断、近似地存一个最接近的值。所以,0.1 在计算机里,存的其实是一个略微偏离 0.1 的近似值;0.2 也是;当这两个各自带着微小误差的近似值相加时,误差累积,结果就成了 0.30000000000000004 这个带着小数尾巴的、不精确的值。这,就是为什么 0.1 + 0.2 !== 0.3。归根结底:我犯了一个最根本的认知错误——我以为"计算机算小数是精确的";可事实是,浮点数,本质上,就是"近似"的;用它去做需要"精确"的计算(尤其是),误差,迟早会浮出水面、酿成对不上账的灾难。

故障现场:浮点数存的是近似值,误差累积

我把这个"算钱算错"的现场,用代码摊开给你看:

// ✗ 灾难: 用浮点数(普通 number)算钱
console.log(0.1 + 0.2);              // 0.30000000000000004  ← 不是 0.3!
console.log(0.1 + 0.2 === 0.3);     // false                ← 比较也不相等!

console.log(0.3 - 0.1);             // 0.19999999999999998
console.log(1.005 * 100);           // 100.49999999999999   ← 想四舍五入到分? 出错!
console.log(Math.round(1.005 * 100) / 100);  // 1   ← 本想得 1.01, 结果是 1!

// 累积起来, 对账就差钱:
let total = 0;
for (let i = 0; i < 10; i++) total += 0.1;
console.log(total);                 // 0.9999999999999999  ← 不是 1.0!

// 为什么? 计算机用二进制 + 有限位数, 存不下无限循环的小数:
//   - 0.1 在十进制很"整", 但换成二进制是"无限循环小数"(像十进制的 1/3)。
//   - 计算机位数有限, 存不下无限循环 → 只能存一个"最接近的近似值"。
//   - 所以 0.1 存的是"略偏离 0.1 的近似值", 0.2 也是。
//   - 两个带误差的近似值相加 → 误差累积 → 0.30000000000000004。

// 这不是 JS 的 bug, 是 IEEE 754 浮点标准的特性(几乎所有语言共有):
//   Python: 0.1 + 0.2 也是 0.30000000000000004。
//   Java/C/Go... 用 float/double 都一样。

// 根因: 浮点数(IEEE 754)无法精确表示大多数十进制小数, 只能近似。
//   用它做需要"精确"的计算(尤其钱), 误差累积 → 算错、对不上账。

看着这些诡异的小数,我才算真正理解了这场"算钱算错"的根源。问题的核心,不在 JavaScript,而是几乎所有语言都共有的、计算机表示小数的根本局限——浮点数(遵循 IEEE 754 标准),根本无法精确地表示大多数十进制小数这要从计算机的存储说起:计算机里,一切都是二进制;而很多在十进制下很"整"的小数(比如 0.1、0.2),换算成二进制,却是"无限循环"的——就像在十进制里,1/3 = 0.333... 除不尽一样,0.1 在二进制里,也是一个无限循环的小数。可计算机,存储一个数的位数是有限的,它存不下一个无限循环的小数,只能截断、近似地,存一个最接近的值。所以:0.1 在计算机里,存的,其实是一个略微偏离 0.1 的近似值;0.2 也是;当这两个各自带着微小误差的近似值相加时,误差累积,结果就成了 0.30000000000000004 这个带着小数尾巴的、不精确的值——这,就是 0.1 + 0.2 !== 0.3 的真相。而且,这些误差会累积:循环加 10 次 0.1,得到的不是 1.0,而是 0.9999999999999999;乘除、四舍五入,都可能因这点误差而出错(1.005 * 100 算出来是 100.49999...,导致本想四舍五入到 1.01,结果错成了 1)。这些误差累积到对账时,金额就差了一两分钱。我要特别强调:不是 JavaScript 的 bug,而是 IEEE 754 浮点标准的特性,几乎所有语言共有——Python 里 0.1 + 0.2 也是 0.30000000000000004,Java/C/Go 用 float/double 也一样。归根结底:我犯了一个最根本的认知错误——我以为"计算机算小数是精确的";可事实是,浮点数,本质上,就是"近似"的;用它去做需要"精确"的计算(尤其是),误差,迟早会浮出水面,酿成对不上账的灾难。

第一件事:搞懂浮点数是"近似"的,不能精确表示多数小数

定位到根源,我必须把"浮点数为什么不精确"彻底搞清楚:

浮点数(IEEE 754): 本质是"近似", 无法精确表示大多数十进制小数

# 为什么浮点数不精确?
# 1. 计算机用"二进制"存数。
# 2. 很多"十进制很整"的小数, 换成二进制是"无限循环小数":
#    - 0.1(十进制) = 0.0001100110011...(二进制, 无限循环)。
#    - (类比: 十进制里 1/3 = 0.333... 也除不尽。)
# 3. 计算机存数的"位数有限"(double 是 64 位), 存不下无限循环。
#    → 只能"截断/舍入", 存一个"最接近的近似值"。
# 4. 所以 0.1 存的是"略偏离 0.1 的值"; 运算时, 误差累积。

# 几个必须知道的事实:
#   - 0.1 + 0.2 !== 0.3(很多语言都这样, 不是 bug)。
#   - 不能用 == / === 直接比较两个浮点数是否相等(可能差一点点)。
#   - 误差会"累积"(多次运算后越来越偏)。
#   - 大整数也有精度问题(超过 2^53 的整数, double 存不准)。

# 关键认知: 浮点数是为"科学计算/范围大、能容忍微小误差"设计的,
#   它用"精度"换"范围"。它"不适合"需要"精确"的场景——尤其是"钱"!

# 哪些场景要警惕?
#   - 金额/财务计算(差一分钱都不行)。
#   - 需要精确相等比较的。
#   - 大量累积运算(误差越积越大)。

# 解药(下一节):
#   - 钱: 用"整数(最小单位, 如分)"算, 或用 decimal/big 类的精确小数库。
#   - 一般浮点比较: 用"误差范围(epsilon)"比, 别用 ==。

# 核心: 浮点数本质是近似的, 不能精确表示多数十进制小数、不能精确比较。
#   需要精确(尤其钱)时, 别用浮点——用整数最小单位, 或精确小数库。

原理终于清晰了。浮点数(IEEE 754)为什么不精确?第一,计算机用二进制存数;第二,很多"十进制很整"的小数,换成二进制是"无限循环小数"(0.1 的二进制 = 0.0001100110011...,无限循环——类比十进制里 1/3 = 0.333... 也除不尽);第三,计算机存数的位数有限(double 是 64 位),存不下无限循环,只能截断/舍入,存一个"最接近的近似值";第四,所以 0.1 存的是"略偏离 0.1 的值",运算时,误差累积由此,有几个必须知道的事实:0.1 + 0.2 !== 0.3(很多语言都这样,不是 bug);不能用 ==/=== 直接比较两个浮点数是否相等(可能差一点点);误差会累积(多次运算后越来越偏);大整数也有精度问题(超过 2^53 的整数,double 存不准)。而一个关键的认知是:浮点数,是为"科学计算、范围大、能容忍微小误差"而设计的——它用"精度"换了"范围";所以,它"不适合"需要"精确"的场景——尤其是"钱"!哪些场景要警惕?金额/财务计算(差一分钱都不行)、需要精确相等比较的、以及大量累积运算的(误差越积越大)。而它的解药(下一节细讲):算钱,用"整数(最小单位,如"分")"来算,或用 decimal/big 这类精确小数库;一般的浮点比较,用"误差范围(epsilon)"来比,别用 ==归根结底:浮点数,本质是近似的,不能精确表示多数十进制小数、也不能精确比较;需要精确(尤其是钱)时,别用浮点——而要用整数的最小单位,或精确的小数库——这,是我用一次"算钱算出诡异小数、还对不上账"的事故,补上的、关于浮点数最关键的一课。

第二件事:正解——算钱用整数最小单位,比较用 epsilon

搞懂了根因——"浮点数是近似的"——正解就清晰了:对于,用"整数 + 最小单位"来算(比如,一律用"分"作单位,用整数存和算,显示时再除以 100),或者用精确小数库(如 decimal.js/big.js);对于一般的浮点数比较,别用 ===,而要看"两数之差,是否在一个极小的误差范围(epsilon)内"。

// 正解1(钱, 最推荐): 用"整数 + 最小单位(分)"算
// 把金额, 一律用"分"(整数)来存和算; 显示时再转成"元"。
const priceCents = 1099;          // 10.99 元 = 1099 分(整数!)
const taxCents = 50;              // 0.50 元 = 50 分
const totalCents = priceCents + taxCents;   // 1149 分(整数加法, 精确!)
console.log((totalCents / 100).toFixed(2)); // "11.49"  ✓ 显示时才转元
// → 整数运算是精确的, 没有浮点误差。金额计算的"黄金法则"。

// 正解2(复杂金额计算): 用精确小数库
// import Big from "big.js";  (或 decimal.js)
// const total = new Big("0.1").plus("0.2");   // "0.3"  ✓ 精确!
// → 库内部用十进制精确表示和运算, 适合复杂的财务计算(汇率、利息等)。

// 正解3(一般浮点比较): 用误差范围 epsilon, 别用 ===
function nearlyEqual(a, b, eps = Number.EPSILON) {
    return Math.abs(a - b) < eps;
}
console.log(nearlyEqual(0.1 + 0.2, 0.3));   // true  ✓ (差在 epsilon 内就算相等)
// (Number.EPSILON 是 JS 里"最小可表示的差"; 实际可按精度需求设 eps)

// 正解4(显示/舍入): 用 toFixed 控制显示位数(但注意它也基于浮点)
(0.1 + 0.2).toFixed(2);    // "0.30"  ← 显示用, 但别拿它做精确计算的依据

// ✗ 永远别这么干:
//   - 用浮点直接算钱(误差累积, 对不上账)。
//   - 用 === 比较两个浮点数(可能差一点点)。
//   - 以为 toFixed/round 能"修复"浮点(它们只是显示/舍入, 底层还是浮点)。

// 核心: 钱用"整数最小单位"或精确小数库(精确); 浮点比较用 epsilon。
//   需要精确的地方, 从根上避开浮点的近似。

这套正解,核心是在需要精确的地方,从根上避开浮点数的近似正解1(钱,最推荐:整数 + 最小单位):把金额,一律用""(整数)来存和算——10.99 元 存成 1099 分,加减都用整数运算(整数运算是精确的,没有浮点误差),只在显示给用户时,才除以 100 转成"元";这是金额计算的"黄金法则"。正解2(复杂金额:精确小数库):对于汇率、利息这类复杂的财务计算,用 decimal.js/big.js 这样的精确小数库——它们内部用十进制来精确表示和运算,Big("0.1").plus("0.2") 得到的就是精确的 "0.3"正解3(一般浮点比较:用 epsilon):比较两个浮点数是否"相等",别用 ===,而要看"它们的差的绝对值,是否小于一个极小的误差范围 epsilon"(Math.abs(a - b) < eps);差在 epsilon 之内,就当它们相等。正解4(显示/舍入):toFixed 可以控制显示的位数,但要注意,它只是用于显示,底层还是浮点,别拿它做精确计算的依据而要永远避免的,是:用浮点直接算钱(误差累积、对不上账)、用 === 比较两个浮点数、以及误以为 toFixed/round 能"修复"浮点(它们只是显示/舍入,底层依然是浮点)。归根结底:钱,用"整数最小单位"或精确小数库(保证精确);一般的浮点比较,用 epsilon(容忍微小误差);在需要精确的地方,从根上避开浮点的近似。我那次的错误,正是用浮点直接算了钱;而正解,就是把钱的计算,换成精确的整数(分)。

下面这张图,对比了"浮点算钱"和"整数分算钱"两条路径:

这张图的对比很清楚:左边红色那条,用浮点数直接算元,0.1+0.2 算出带尾巴的近似值、误差累积、比较失败,显示和对账差一两分钱、算错钱;右边绿色那几条,用整数"分"来算(整数运算精确、显示时才除 100 转元)、或用 big.js 等精确小数库,金额分毫不差。两条路的根本分野,在于你算钱用的,是近似的浮点,还是精确的整数/小数库。

第三件事:浮点和数字精度的其它坑

填平了算钱这个坑,我系统排查了一遍 JS(及通用)数字精度的其它常见坑:

// 数字精度的其它坑:

// 1. 浮点算钱(本文): 用整数分 / 精确小数库。

// 2. 用 === 比较浮点(本文): 用 epsilon。

// 3. toFixed/round 的"假修复": 它们只影响显示/舍入, 不改变底层浮点性质。
//    且 toFixed 的舍入有时也反直觉(基于浮点): (1.005).toFixed(2) → "1.00"(不是1.01)

// 4. 大整数超出安全范围: JS 的 number 是 double, 整数超过 2^53 就不精确。
console.log(9007199254740992 + 1);   // 9007199254740992 (没加上去!超 2^53)
// → 大整数用 BigInt: 9007199254740992n + 1n。(如订单号、雪花 ID, 别用 number 存)

// 5. parseFloat/parseInt 的边界、NaN 的传染:
//    NaN === NaN 是 false; 任何运算遇 NaN 都成 NaN; 用 Number.isNaN 判断。

// 6. 不同单位/精度混算: 别把"元"和"分"、不同精度的数混着算。

// 7. 累积误差: 大量浮点运算后误差变大 → 关键计算用精确方式。

// 8. JSON 传输大整数: 后端 long 传到 JS 变 number 可能丢精度
//    → 后端把大整数序列化成"字符串"传给前端。

// 共同点: 都源于"数字在计算机里的表示是有限/近似的"。
// 原则: 钱用整数分/精确库; 大整数用 BigInt/字符串; 浮点比较用 epsilon。
//   需要精确的地方, 别依赖浮点的"看起来对"。

这一排查,让我对数字精度的"雷区",有了全面的认识。除了浮点算钱(本文),还有几个常见坑:=== 比较浮点(本文,用 epsilon);toFixed/round 的"假修复"(它们只影响显示/舍入,不改变底层的浮点性质,而且 toFixed 的舍入有时也反直觉——(1.005).toFixed(2) 得到的是 "1.00" 而非 "1.01");大整数超出安全范围(JS 的 number 是 double,整数超过 2^53 就不精确——9007199254740992 + 1 竟然没加上去;大整数如订单号、雪花 ID,要用 BigInt,别用 number 存);NaN 的传染(NaN === NaN 是 false、任何运算遇 NaN 都变 NaN,要用 Number.isNaN 判断);不同单位/精度混算(别把"元"和"分"混着算);累积误差(大量浮点运算后误差变大);JSON 传输大整数丢精度(后端的 long 传到 JS 变 number 可能丢精度,应让后端把大整数序列化成字符串传给前端)。这些坑的共同点,都源于"数字在计算机里的表示,是有限的、近似的"。所以,核心原则就是:钱,用整数分或精确库;大整数,用 BigInt 或字符串;浮点比较,用 epsilon;一句话——在需要精确的地方,别依赖浮点那"看起来对"的假象。把这条刻在心里,数字精度的这些坑,就都能被你避开。

第四件事:为什么计算机存不下 0.1——浮点的二进制本质

为了把这个坑彻底吃透,我又往下挖了一层,搞懂了浮点数在二进制里到底是怎么存的、为什么 0.1 就是存不准:

浮点的二进制本质: 为什么 0.1 存不准

# 十进制小数 → 二进制小数, 怎么转?
#   - 整数部分: 除 2 取余。
#   - 小数部分: 乘 2 取整(取整数位)。
#   - 0.1 乘 2 取整: 0.1×2=0.2(0), 0.2×2=0.4(0), 0.4×2=0.8(0),
#     0.8×2=1.6(1), 0.6×2=1.2(1), 0.2×2=0.4(0)... → 0.000110011001100...
#   → 0.1 = 0.0001100110011...(二进制), "0011"无限循环! 永远除不尽。

# 类比: 就像十进制里, 1/3 = 0.3333... 除不尽一样。
#   - 一个数能不能"有限位表示", 取决于"进制"。
#   - 0.1 在十进制有限(就一位), 但在二进制无限循环。
#   - (二进制下, 只有分母是 2 的幂次的小数, 才能有限表示, 如 0.5、0.25。)

# 计算机怎么存(IEEE 754 double, 64 位)?
#   - 1 位符号 + 11 位指数 + 52 位尾数(有效数字)。
#   - 尾数只有 52 位 → 存不下无限循环 → 截断到 52 位, 存"最接近的近似值"。
#   - 所以存进去的 0.1, 其实是"约等于 0.1 的那个最接近的可表示值"。

# 误差从哪来:
#   - 存 0.1 时, 就有了"舍入误差"(存的不是精确的 0.1)。
#   - 0.1 + 0.2: 两个近似值相加, 结果再舍入 → 0.30000000000000004。
#   - 多次运算 → 误差累积。

# 所以:
#   - "能精确表示"的小数: 分母是 2 的幂(0.5, 0.25, 0.125...)。
#   - "不能精确"的: 0.1, 0.2, 0.3 等大多数十进制小数。
#   → 这是二进制浮点的"先天局限", 不是哪个语言的 bug。

# 核心: 0.1 在二进制是无限循环小数, 而尾数位有限 → 只能存近似值。
#   理解了这个底层, 就明白浮点误差是必然的, 该精确时就别用它。

这一深挖,让我从"知道浮点不精确"到了"知道它为什么必然不精确"。先看十进制小数怎么转二进制:小数部分,用"乘 2 取整"——0.1×2=0.2(取 0)、0.2×2=0.4(0)、0.4×2=0.8(0)、0.8×2=1.6(1)、0.6×2=1.2(1)、0.2×2=0.4(0)……会发现它开始循环了;所以,0.1 = 0.0001100110011...(二进制),"0011"无限循环、永远除不尽这就像类比:十进制里,1/3 = 0.333... 也除不尽;一个数能不能"有限位表示",取决于进制——0.1 在十进制是有限的(就一位),但在二进制里却是无限循环的(在二进制下,只有分母是 2 的幂次的小数,如 0.5、0.25,才能有限表示)。计算机怎么存(IEEE 754 double,64 位)?1 位符号 + 11 位指数 + 52 位尾数;尾数只有 52 位,存不下无限循环,只能截断到 52 位、存一个"最接近的近似值";所以,存进去的 0.1,其实是"约等于 0.1 的、那个最接近的可表示值"。误差,就是这么来的:0.1 时,就有了舍入误差(存的不是精确的 0.1);0.1 + 0.2,是两个近似值相加、结果再舍入,于是成了 0.30000000000000004;多次运算,误差累积由此,就能分清:"能精确表示"的小数,是分母为 2 的幂的(0.5、0.25、0.125);"不能精确"的,是 0.1、0.2、0.3 等大多数十进制小数——这是二进制浮点的"先天局限",不是哪个语言的 bug归根结底:0.1 在二进制里是无限循环小数,而尾数位有限,所以只能存近似值;理解了这个底层,就明白了浮点误差是必然的,该精确的时候,就别用它。把"能/不能精确表示"的小数,整理成一张表:

小数 分母 二进制 能精确表示?
0.5 2(2的幂) 0.1
0.25 4(2的幂) 0.01
0.1 10(非2幂) 0.00011...循环 不能(近似)
0.2 / 0.3 非2幂 无限循环 不能(近似)

第五件事:别假设抽象是"完美无瑕"的,要懂它的物理局限

这次踩坑,在认知层面给了我最大的纠偏——它让我明白,再基础的抽象,也有它的物理局限。我把这层反思,沉淀了下来:

认知纠偏: 别假设抽象"完美无瑕", 它背后有物理/实现的局限

# 我的误解(错误的):
#   我默认"数字就是数字, 计算机算数当然精确"——把"数"这个抽象,
#   当成了完美的、和数学上一模一样的东西。
#   → 没意识到, "数"在计算机里, 是用"有限的二进制"近似实现的。

# 真相: 计算机里的抽象, 都受"物理/实现"的约束, 不是完美的数学概念
#   - "数": 数学上无限精确, 但计算机用有限位表示 → 浮点近似、整数溢出。
#   - "无限": 计算机没有真正的无限(内存有限)。
#   - "瞬间": 没有真正的瞬间(一切操作都有耗时)。
#   - "可靠": 没有绝对可靠(硬件会坏、网络会断)。
#   → 这些"理想的抽象", 落到真实的计算机上, 都有它的"漏洞/局限"。

# "漏抽象法则(Leaky Abstractions)"再次应验:
#   - 抽象(如"数字")让你大部分时候不用关心底层(二进制存储)。
#   - 但在边界(如精度敏感的金额计算), 底层的实现细节会"泄漏"出来。
#   - 你不懂底层, 就会在这些边界被"看似完美的抽象"反咬一口。

# 正确的习惯:
#   1. 别假设抽象是"完美"的——了解它"用什么实现的、有什么局限"。
#   2. 在"精度/边界/极限"敏感的场景, 尤其要懂底层(如金额、大数、时间)。
#   3. 知道每个抽象"适用什么、不适用什么"(浮点适合科学计算, 不适合钱)。

# 核心: 计算机的抽象有物理/实现的局限, 不是完美的数学概念。
#   懂它的底层和局限, 才能在边界场景(如精确算钱)用对、不被坑。

这层反思,是这次踩坑给我最高维度的收获。复盘我的误解,根源是:我默认"数字就是数字,计算机算数当然精确"——我把"数"这个抽象,当成了一个完美的、和数学上一模一样的东西;没意识到,"数"在计算机里,是用"有限的二进制"近似实现的。可真相是:计算机里的抽象,都受"物理/实现"的约束,而不是完美的数学概念看几个例子:""——数学上无限精确,但计算机用有限位表示,于是有了浮点近似、整数溢出;"无限"——计算机没有真正的无限(内存有限);"瞬间"——没有真正的瞬间(一切操作都有耗时);"可靠"——没有绝对可靠(硬件会坏、网络会断);这些"理想的抽象",落到真实的计算机上,都有它的"漏洞/局限"这,正是"漏抽象法则(Leaky Abstractions)"的又一次应验:抽象(如"数字"),让你大部分时候不用关心底层(二进制存储);但在边界(如精度敏感的金额计算),底层的实现细节,会"泄漏"出来;你不懂底层,就会在这些边界,被那个"看似完美的抽象",反咬一口(就像我,被"数字"这个抽象,在算钱时坑了)。由此,我立下了几条习惯:第一,别假设抽象是"完美"的——去了解它"用什么实现的、有什么局限";第二,在"精度/边界/极限"敏感的场景(如金额、大数、时间),尤其要懂底层;第三,知道每个抽象"适用什么、不适用什么"(浮点适合科学计算,不适合算钱)。归根结底:计算机的抽象,有物理/实现的局限,它不是完美的数学概念;懂它的底层和局限,才能在边界场景(如精确算钱)用对、不被坑。我那串诡异的小数尾巴,正是"数字"这个看似完美的抽象,在精度边界上,向我泄漏出的物理真相。把"以为抽象完美"和"懂其局限"对比成一张表:

维度 以为抽象完美(踩坑) 懂其物理局限(成熟)
对"数字" 以为永远精确 知道浮点近似/整数溢出
认知 当完美数学概念 有限二进制的近似实现
边界场景 被抽象反咬 知道底层会泄漏,提前防
算钱 直接用浮点 用整数分/精确库
对抽象 无脑信任 知其适用与局限

一套"数字计算该怎么选"的决策流程

把这次踩坑的全部教训,我浓缩成了一张"做数字计算、该怎么选类型/方法"的决策图,贴在了团队的规范里:

这张图,把我"血泪换来"的整套方法论,串成了一条可执行的路径:做数字计算,先问是不是金额/财务(差一分都不行)——是,且简单加减就用整数最小单位(分)算、显示再除 100,复杂(汇率/利息)就用精确小数库;不是金额的一般浮点计算,要比较相等就用 epsilon(别用 ===)。另外,大整数(超过 2^53,如订单号/ID)要用 BigInt 或让后端传字符串。这条"按是否算钱、是否精确比较、是否大整数选方法"的决策链,现在是我们团队做每一个数字计算时的准则。

我立下的几条数字精度规矩

这次"浮点算钱算错"的踩坑,让我把数字精度的注意事项,认真地立成了几条规矩:

  1. 记牢浮点是近似的,0.1+0.2≠0.3。大多数十进制小数二进制下无限循环,只能存近似值,这是 IEEE 754 特性、非 bug。
  2. 算钱用整数最小单位。金额一律用"分"(整数)存和算,显示时才除 100,整数运算精确。
  3. 复杂金额用精确小数库。汇率/利息等用 big.js/decimal.js,内部十进制精确运算。
  4. 浮点比较用 epsilon。看两数之差是否在极小误差内,别用 ===。
  5. 大整数用 BigInt/字符串。超过 2^53 的整数(订单号/ID)number 存不准;后端大整数序列化成字符串传前端。
  6. 别信 toFixed/round 能修复浮点。它们只影响显示/舍入,底层还是浮点。
  7. 懂抽象的物理局限。计算机的"数字"不是完美数学概念;精度敏感处要懂底层、别无脑信任。

写在最后

这次"我用 JavaScript 算钱、0.1+0.2 居然不等于 0.3、还对不上账"的经历,是我在编程路上,一次很经典、也很受用的成长。它教给我的,远不止"算钱用整数分"这一条具体的技术经验,更是一个关于抽象的根本认知——计算机里的抽象,都有它的物理和实现局限,它不是完美的数学概念。我那串诡异的小数尾巴,根源就在于,我把"数字"这个抽象,当成了完美的、和数学上一模一样的东西;却不知道,"数"在计算机里,是用有限的二进制近似实现的——0.1 在二进制里是个无限循环小数,根本存不准,于是,这个"看似完美"的抽象,在我精确算钱的边界上,泄漏出了它近似的物理真相。

所以,当你使用任何一个看似"理所当然、完美无瑕"的抽象时(数字、无限、瞬间、可靠),请别想当然地全盘信任它——而要去了解:它,到底是用什么实现的?有什么物理/实现的局限?在什么"边界场景"下,它的不完美,会泄漏出来?就像浮点数,你只要知道"它本质是近似的、算钱会出错",就会在涉及金额时,果断地改用整数分或精确小数库,绝不会再经历那种"算出诡异小数、还对不上账"的窘境。从"以为抽象完美"到"懂它的物理局限",从无脑用浮点算钱到为精确场景选对工具,是从一个"会写计算"的开发,走向一个"懂底层、在边界上靠谱"的工程师,必经的修炼。愿你算的每一笔钱,都分毫不差、对得上账;也愿你我,在享受每一个抽象带来的便利时,都记得它背后,那有限而真实的、物理的边界。共勉。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

我用多线程跑 CPU 密集的计算想给程序加速,结果开了好几个线程不但没快、反而比单线程还慢,我盯着这个反常的结果查了大半天才搞懂 GIL 的深度复盘

2026-6-2 1:13:21

技术教程

我的 Go 服务内存和 goroutine 数量只涨不跌、跑久了必 OOM,最后揪出是一堆 goroutine 永远卡在 channel 上、泄漏了出不来的深度复盘

2026-6-2 1:27:01

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索