谈谈 js 小数加法精度问题

一、问题现象

我们从一个最常见的例子说起:

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,所以在日常开发中掌握其解决办法,可以提前避免相关问题。

相关推荐
发现一只大呆瓜18 分钟前
虚拟列表:支持“向上加载”的历史消息(Vue 3 & React 双版本)
前端·javascript·面试
css趣多多35 分钟前
ctx 上下文对象控制新增 / 编辑表单显示隐藏的逻辑
前端
阔皮大师41 分钟前
INote轻量文本编辑器
java·javascript·python·c#
lbb 小魔仙41 分钟前
【HarmonyOS实战】React Native 表单实战:自定义 useReactHookForm 高性能验证
javascript·react native·react.js
_codemonster42 分钟前
Vue的三种使用方式对比
前端·javascript·vue.js
寻找奶酪的mouse42 分钟前
30岁技术人对职业和生活的思考
前端·后端·年终总结
梦想很大很大1 小时前
使用 Go + Gin + Fx 构建工程化后端服务模板(gin-app 实践)
前端·后端·go
We་ct1 小时前
LeetCode 56. 合并区间:区间重叠问题的核心解法与代码解析
前端·算法·leetcode·typescript
张3蜂1 小时前
深入理解 Python 的 frozenset:为什么要有“不可变集合”?
前端·python·spring
无小道1 小时前
Qt——事件简单介绍
开发语言·前端·qt