一、问题现象
我们从一个最常见的例子说起:
js
console.log(0.1 + 0.2);
这行代码:0.1 加 0.2,应该是等于0.3的,但实际上并不是

js
console.log(0.1 + 0.7 === 0.8);
这行代码:0.1 加 0.7,应该是等于0.8的,打印应该为true,实际并不是

这种微小的偏差看似没什么大问题,但如果你用它来作条件判断
js
console.log(0.1 + 0.7 === 0.8); // 输出:false
这就可能直接导致业务逻辑错误,是个严重的bug。
二、为什么会出现精度丢失
这个问题的根源在于计算机内部使用二进制来表示数字,而许多十进制小数无法在二进制中精确表示。
在二进制中,0.1 实际上是一个无限循环的小数,就像十进制中 1/3 = 0.333...
一样。IEEE 754 浮点数标准中,只能保留有限的位数,因此这种无限循环的二进制小数会被截断,从而造成精度损失。
当我们写下 0.1 时,计算机实际存储的是最接近 0.1 的二进制表示,这个值略微不等于真正的 0.1。就像我们无法用十进制精确表示 1/3 一样,计算机也无法用二进制精确表示某些十进制小数。
简单来说:
- 十进制的 0.1 在二进制中表示为:
0.00011001100110011...(循环)
- 二进制截断后转换为十进制,就是我们看到的
0.10000000000000000555...
多个这种带误差的数相加,自然就出现了结果偏差。
三、常见解决方案及代码示例
1. 使用 toFixed
或 toPrecision
限制精度
js
let result = (0.1 + 0.2).toFixed(2); // '0.30' 字符串
console.log(result); // 输出:'0.30'
console.log(Number(result)); // 输出:0.3
优点: 简单直观
缺点: 返回的是字符串,需手动转为数字;四舍五入可能带来新的误差
2. 放大整数再计算,再缩小回去
这是金融类系统中常用的思路。比如先将 0.1 和 0.2 变为 10 和 20,相加后再除以 10。
js
function add(a, b) {
let base = Math.pow(10, Math.max(decimalLength(a), decimalLength(b)));
return (a * base + b * base) / base;
}
function decimalLength(num) {
const parts = num.toString().split('.');
return parts[1] ? parts[1].length : 0;
}
console.log(add(0.1, 0.2)); // 输出:0.3
优点: 精度高,适用于加减法
缺点: 实现稍复杂,乘除法需额外处理
3. 使用第三方库(推荐)
对于需要高精度计算的场景,建议使用成熟库来处理,比如:
✅ decimal.js(体积更大 几十KB)
js
npm install decimal.js
js
import Decimal from 'decimal.js';
const result = new Decimal(0.1).plus(0.2);
console.log(result.toNumber()); // 输出:0.3
✅ big.js 体积更小 几KB)
js
npm install big.js
js
import Big from 'big.js';
const result = new Big(0.1).plus(0.2);
console.log(result.toNumber()); // 输出:0.3
两者对比
特性 | decimal.js |
big.js |
---|---|---|
精度控制 | 支持任意精度,功能更丰富 | 精度控制简单,但够用 |
API 接口 | .plus() , .minus() , .times() 等 |
同样使用 .plus() 等一致方法名 |
文件体积 | 较大(几十 KB) | 更轻量(几 KB) |
功能 | 支持三角函数、开方、对数等复杂运算 | 更专注于基本加减乘除 |
精度表现 | 非常高 | 同样可靠 |
如果只是简单加减,没有复杂的运算,推荐big.js
,因为体积更小。
总结
js 中小数相加产生精度丢失,并非语言缺陷,而是由于底层 IEEE 754 双精度浮点数的表示机制所致。
像 0.1 + 0.2 !== 0.3
的问题,在日常开发中常常被忽视,却可能在关键业务场景中引起问题,从而出现bug,所以在日常开发中掌握其解决办法,可以提前避免相关问题。