摘要:你可能心想:这不就三毛钱的事吗,咋还算不准?别说前端开发,就算是老后端看了都要皱眉。但这其实不是 JavaScript 独有的问题,几乎所有使用 IEEE 754 双精度浮点数的语言(像 Java、Python、C++)都会遇到。只是 JavaScript 没
很多人第一次接触 JavaScript 的时候,都会被这个现象震惊:
console.log(0.1 + 0.2); // 0.30000000000000004你可能心想:这不就三毛钱的事吗,咋还算不准?别说前端开发,就算是老后端看了都要皱眉。但这其实不是 JavaScript 独有的问题,几乎所有使用 IEEE 754 双精度浮点数的语言(像 Java、Python、C++)都会遇到。只是 JavaScript 没有内置整数类型,所有数字都用这种浮点型来表示,所以精度问题更常见。
一切从二进制说起
我们平时写的数字是十进制,但计算机只认识二进制。 像 0.1 这样的十进制小数,其实无法被二进制精确表示,就像我们无法用有限位数写出无限循环小数 1/3 = 0.333... 一样。
0.1 在二进制中是个无限循环的形式,大致是:
0.0001100110011001100110011...(无限)所以在存入内存时,计算机只能“截断”它,用有限的 64 位去存储一个“近似值”。0.2 也是类似的“近似值”。当你让两个“近似值”相加,结果自然不一定精确是 0.3,而是“差一点点”。 而这“差一点点”在浮点表示下就成了 0.30000000000000004。
这并不是 JavaScript 算错了,而是 计算机本身精度有限,你要的是十进制精确算术,但机器做的是二进制近似计算。
为什么 JavaScript 特别容易中招:
JavaScript 的所有数字类型(除了 ES2020 引入的 BigInt)都是用 64 位 IEEE 754 双精度浮点数 存储的。这意味着:
1 位是符号位; 11 位是指数; 52 位是尾数(有效数字部分)。
这套表示方式能表达的范围很大(约 ±10^308),但有效数字只有 15~17 位左右。 所以当你在 JS 中执行加减乘除,尤其涉及小数时,精度丢失的概率非常高。 比如:
0.1 * 0.2 // 0.0200000000000000040.07 * 100 // 7.000000000000001这些值都“接近正确”,但永远不是完全相等。
金额计算: 这可能是最危险的场景。比如 0.1 + 0.2 代表三毛钱,那多次计算后的误差就可能变成几分钱。 判断相等: 比如 if (0.1 + 0.2 === 0.3),结果会是 false。 循环累计误差: 多次浮点运算累积后,小误差会被放大,导致最终结果不稳定。
这些问题的本质都在于:JS 的浮点数无法保证精确的小数运算。
怎么解决:
解决精度问题主要有三种思路:缩放法、库函数法、以及新类型 BigInt。
缩放法:
最常见、最实用的方法就是:把小数转成整数运算,再缩回来。 例如,我们把 0.1 看成 10 分、0.2 看成 20 分:
function add(a, b) { const factor = 10 ** 10; // 放大倍数 return (Math.round(a * factor) + Math.round(b * factor)) / factor;}console.log(add(0.1, 0.2)); // 0.3这个思路很简单:在计算前统一放大,再计算,再缩回去。 但要注意,放大倍数必须足够大,否则依然可能出现误差。适合在你能确定小数位数有限(如金额最多两位)的场景。
使用高精度库(如 decimal.js / big.js)
它们的原理是 用字符串模拟十进制计算,手动处理每一位数字,不依赖浮点表示。 例子:
import { Decimal } from 'decimal.js';const x = new Decimal(0.1);const y = new Decimal(0.2);console.log(x.plus(y).toNumber); // 0.3这种方式更稳定,也能避免舍入误差。不过性能肯定会慢一点,毕竟是“软件模拟”十进制加法。
ES2020 的 BigInt(整数精确)
BigInt 是 JS 新增的整数类型,可以安全地表示任意大整数,不受 2^53 的限制:
const a = 9007199254740991n;console.log(a + 1n === a + 2n); // false,说明没丢精度但注意:BigInt 不能表示小数。 所以金额类问题(需要小数)依然要用上面两种方式。BigInt 主要解决的是大数精度,不是小数精度。
那浮点误差能不能“自动修正”?
有时候我们其实不需要完美精确,只要“看起来对”。 比如保留两位小数显示时,可以直接:
(0.1 + 0.2).toFixed(2); // "0.30"或者:
Math.round((0.1 + 0.2) * 100) / 100; // 0.3这种方式只是显示修正,并没有改变内部的误差值。 如果只是展示给用户看,这样完全够用;但如果涉及存储或计算(比如金额结算),必须在底层处理精度问题。
拓展:整数范围问题
除了小数外,JS 的整数也有“最大安全值”。 因为尾数部分只有 52 位,能安全表示的整数范围是:
Number.MIN_SAFE_INTEGER = -9007199254740991Number.MAX_SAFE_INTEGER = 9007199254740991超过这个范围的整数会丢失精度:
console.log(9007199254740992 === 9007199254740993); // true这就是为什么在处理大数(如订单号、身份证号)时,最好用字符串或 BigInt。
实际项目中怎么选:
如果只是小数展示问题,用 toFixed 或 Math.round。 如果是金额或计量等敏感计算,使用“放大整数法”。 如果是复杂数学计算或金融业务,用 decimal.js 这样的库。 如果是大整数计算(加密、时间戳、编号),用 BigInt。
一般来说,精度问题永远无法“彻底消失”,只能根据业务重要性和性能要求找到平衡点。
一句话总结:
JavaScript 的数字精度丢失,说白了就是:二进制没法精确表示某些十进制小数,再加上浮点存储位数有限,造成运算误差。解决思路不是修复语言,而是避开浮点陷阱:要么放大成整数再算,要么用库处理十进制,要么用 BigInt 处理整数。
所以别再问“为啥 0.1 + 0.2 不等于 0.3”了,计算机不是算错了,它只是按照自己的逻辑——二进制世界的逻辑——在忠实执行你的代码而已。
来源:不秃头程序员
