JavaScript 中数字精度丢失是语言底层设计导致的必然现象,核心源于 JS 采用 IEEE 754 双精度浮点数 标准存储数字,该标准无法精确表示所有十进制小数(如 0.1、0.2),进而引发计算误差。以下从「根源解析」「常见场景」「解决方案」三方面彻底讲清。
一、核心原因:IEEE 754 双精度浮点数的局限性
1. 浮点数的存储规则
JS 中所有数字(整数、小数)都以 64 位双精度浮点数 存储,结构如下:
| 位段 | 符号位(S) | 指数位(E) | 尾数位(M) |
|---|---|---|---|
| 位数 | 1 位 | 11 位 | 52 位 |
| 作用 | 正负(0 = 正,1 = 负) | 表示指数(偏移值 1023) | 表示有效数字(隐含首位 1) |
核心限制:
- 尾数位(52 位)决定了 有效数字最多只能精确到 53 位二进制(约 16 位十进制);
- 十进制小数(如 0.1)转换为二进制时,大概率是「无限循环小数」,但尾数位只能存储 52 位,必然截断,导致存储值与原值存在微小偏差。
2. 直观示例:0.1 + 0.2 ≠ 0.3 的本质
console.log(0.1 + 0.2); // 输出 0.30000000000000004
拆解过程:
- 0.1 的十进制转二进制:
0.0001100110011...(无限循环); - 0.2 的十进制转二进制:
0.001100110011...(无限循环); - 二者存储时被截断为 52 位尾数位,相加后再转回十进制,就出现了精度偏差。
3. 整数的精度丢失场景
很多人误以为只有小数会丢失精度,但 JS 整数超过 2^53(9007199254740992) 后,也会无法精确表示:
console.log(9007199254740992 + 1); // 9007199254740992(错误,应为 9007199254740993)
console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991(最大安全整数)
原因:53 位二进制有效数字只能精确表示到 2^53,超过后无法区分相邻整数(如 2^53 和 2^53+1 存储为同一个值)。
二、常见精度丢失场景
- 小数运算:0.1+0.2、0.7*10、1.0-0.9 等;
- 大整数处理:后端返回的雪花 ID(18 位以上)、订单号等;
- 货币计算:金额(如 0.01 元)累加 / 乘法;
- 数据序列化:JSON 传输大整数时被截断(如
JSON.parse('9007199254740993')结果为 9007199254740992)。
三、解决方案(按场景选型)
方案 1:放大法(适合简单小数运算,如货币)
核心思路:将小数转换为整数运算,结果再缩小对应倍数,避免浮点数直接计算。
// 加法:0.1 + 0.2
function add(a, b) {
const decimalA = (a.toString().split('.')[1] || '').length; // 小数位数
const decimalB = (b.toString().split('.')[1] || '').length;
const maxDecimal = Math.max(decimalA, decimalB);
const multiple = Math.pow(10, maxDecimal); // 放大倍数
// 转为整数运算,避免精度丢失
return (a * multiple + b * multiple) / multiple;
}
console.log(add(0.1, 0.2)); // 0.3
// 货币计算(如 0.01 + 0.02)
console.log((0.01 * 100 + 0.02 * 100) / 100); // 0.03
⚠️ 注意:放大倍数不能超过 2^53(如 10^16 以内),否则整数也会丢失精度。
方案 2:使用 Number.EPSILON 做误差判断(适合比较场景)
Number.EPSILON 是 JS 表示的最小精度(≈ 2.22e-16),用于判断两个数的差值是否在误差范围内:
// 判断两个数是否“实际相等”
function isEqual(a, b) {
return Math.abs(a - b) < Number.EPSILON;
}
console.log(isEqual(0.1 + 0.2, 0.3)); // true
console.log(0.1 + 0.2 === 0.3); // false
方案 3:原生 BigInt(适合大整数处理,ES6+)
BigInt 是 ES6 新增的原生类型,可精确表示任意大整数,解决 2^53 以上整数的精度问题:
// 声明 BigInt:数字后加 n 或用 BigInt() 转换
const bigNum1 = 9007199254740993n;
const bigNum2 = BigInt('9007199254740993');
console.log(bigNum1 === bigNum2); // true
// 大整数运算
console.log(9007199254740992n + 1n); // 9007199254740993n(正确)
// 注意:BigInt 不能与 Number 混合运算
console.log(1n + 1); // 报错,需统一类型:1n + BigInt(1)
✅ 适用场景:雪花 ID、订单号、大整数传输 / 存储。
方案 4:第三方库(适合复杂运算,推荐生产环境)
对于复杂的小数 / 大整数运算(如金融系统),推荐使用成熟库,避免手写逻辑出错:
(1)decimal.js(功能最全)
npm install decimal.js
import Decimal from 'decimal.js';
// 0.1 + 0.2
const a = new Decimal(0.1);
const b = new Decimal(0.2);
console.log(a.plus(b).toNumber()); // 0.3
// 大整数运算
const bigNum = new Decimal('9007199254740993');
console.log(bigNum.plus(1).toString()); // 9007199254740994
(2)math.js(轻量,适合通用计算)
npm install mathjs
import { add, multiply } from 'mathjs';
console.log(add(0.1, 0.2)); // 0.3
console.log(multiply(0.7, 10)); // 7(而非 6.999999999999999)
(3)big.js(极简,仅处理小数)
专注小数精度,体积最小(≈ 3KB):
npm install big.js
import Big from 'big.js';
console.log(Big(0.1).plus(0.2).toNumber()); // 0.3
方案 5:序列化大整数(JSON 传输场景)
后端返回的大整数(如 18 位 ID)通过 JSON 传输时,会被 JS 解析为不精确的浮点数,解决方案:
-
后端将大整数转为字符串返回;
-
前端用
BigInt()转换后处理:// 后端返回:{ "id": "9007199254740993" } const res = JSON.parse('{ "id": "9007199254740993" }'); const id = BigInt(res.id); // 9007199254740993n(精确)
四、避坑指南
-
避免直接比较浮点数:永远不要用
===比较 0.1+0.2 和 0.3,改用Number.EPSILON或库; -
货币计算优先放大法 / 库:金额运算必须保证精确,禁止直接浮点数相加;
-
大整数优先用 BigInt:超过 2^53 的整数,一律用 BigInt 处理;
-
选择合适的库:
- 金融场景:decimal.js;
- 通用场景:math.js;
- 极简需求:big.js。
总结
JS 数字精度丢失的本质是「IEEE 754 双精度浮点数无法精确表示所有十进制数」,解决思路分三类:
- 简单小数运算:放大法 + Number.EPSILON;
- 大整数处理:BigInt;
- 复杂 / 金融运算:decimal.js/math.js 等专业库。
生产环境中,优先使用成熟库而非手写逻辑,可最大程度避免精度问题。