最近在写业务逻辑时,遇到一个非常诡异的 Bug。
数据库里的 User ID 是 101092432494557966,但是前端请求回来的数据里,ID 竟然变成了 101092432494557970!
尾数的 66 变成了 70,导致前端拿这个 ID 去查详情时,直接报 404 Not Found。
这简直是“灵异事件”。查了一圈代码,发现罪魁祸首竟然是 JavaScript 的 Number 类型。
// 模拟案发现场
const dbId = 101092432494557966n; // BigInt 从数据库出来
const apiResponse = Number(dbId); // 手贱转成了 Number
console.log(apiResponse);
// 输出: 101092432494557970 😱
为什么会这样?这就引出了今天的主角:BigInt 与 Number。
🔍 原理剖析:Number 的“安全区”
JavaScript 的 Number 类型本质上是 IEEE 754 双精度浮点数。
它不是无限精度的,它只有 53 个二进制位 用来存储有效数字(尾数)。
1. 安全整数范围
JS 能“精准”表示的最大整数是 Number.MAX_SAFE_INTEGER,即 。
数值为:9,007,199,254,740,991(约 9 千万亿)。
2. 精度丢失的真相
一旦数字超过这个安全范围(比如上面的 18 位 ID),Number 的“刻度尺”就不够用了。它无法分辨 ...966 和 ...970 之间的微小差别,只能进行“四舍五入”,强制对齐到最近的一个可表示数值。
这就是为什么你的 ID 会变异。
⚔️ BigInt 的救赎与“三宗罪”
为了解决这个问题,ES2020 推出了 BigInt。它可以表示任意大的整数。
看起来很美好对吧?数据库里的 bigint 字段直接映射成 JS 的 BigInt 好像很完美。
但是,在实际工程中,BigInt 其实是个“刺头”。 很多资深开发者的原则是:除非迫不得已,尽量用 Number。
为什么?因为 BigInt 有三大“工程痛点”:
痛点一:JSON 序列化炸弹(最致命)
这是 Node.js/NestJS 开发中最容易踩的坑。
JSON.stringify 不支持 BigInt!
const user = {
id: 100n,
name: "John"
};
JSON.stringify(user);
// ❌ 报错: TypeError: Do not know how to serialize a BigInt
如果你的接口直接返回了包含 BigInt 的对象,整个请求会直接 500 挂掉。你必须手动处理每一个字段,或者配置全局拦截器,非常麻烦。
痛点二:生态隔离
JavaScript 生态中 99% 的第三方库(Lodash, Chart.js, Mathjs, Excel 导出库等)都是基于 Number 设计的。
如果你传一个 BigInt 进去,它们大概率会报错,或者计算出奇怪的结果。为了兼容,你不得不转来转去,代码写得像通心粉。
痛点三:严苛的类型运算
JS 不允许 BigInt 和 Number 混合运算,必须显式转换。
const money = 100n;
const tax = 0.1;
// ❌ 报错: Cannot mix BigInt and other types
const total = money + tax;
这在业务逻辑复杂的代码中,会增加大量的心智负担。
💡 最佳实践:怎么选?怎么存?怎么传?
既然 Number 有精度上限,BigInt 又难用,我们在项目中到底该怎么办?
以下是经过实战验证的 “黄金法则” 。
1. 什么时候必须用 BigInt?
只有满足以下条件时,才引入 BigInt:
- 分布式 ID (Snowflake) :如 Twitter 雪花算法生成的 ID(18-19 位),必超 Number 安全范围。
- 高精度时间戳:纳秒级计时。
- 加密货币/金融:涉及比特币等超大数值计算。
2. 什么时候坚决用 Number?
- 金额(分) :只要业务不超过 9 千万亿(相信我,你公司的业务很难超过这个数),存“分”用 Number 绰绰有余。
- 普通自增 ID:MySQL 的自增主键。
- 数量、库存、点赞数。
理由:享受原生 JSON 支持,享受极致的 CPU 浮点运算速度,兼容所有第三方库。
3. 工程化解决方案(核心)
针对“ID 精度丢失”和“JSON 报错”问题,标准的解决链路如下:
-
数据库层:必须用
BIGINT或VARCHAR存储,确保源头不丢。 -
后端计算层:
- 如果是 ID:用
BigInt或String传递。 - 如果是金额:用
Number计算(在安全范围内)。
- 如果是 ID:用
-
接口返回层(DTO)—— 关键一步!
千万不要把 BigInt 直接丢给前端!
千万不要把 BigInt 转成 Number 丢给前端!
✅ 正确做法:在返回给前端的那一刻,把 大数 ID 转成 String。
// NestJS DTO 示例
import { Transform } from 'class-transformer';
export class UserDto {
// 1. 数据库里是 BigInt
// 2. 这里的 Transform 负责在序列化给前端时,转成 String
@Transform(({ value }) => String(value))
unionId: string;
// 金额如果没超限,直接用 Number
balance: number;
}
前端 JavaScript 处理大数 ID 的唯一标准方案就是:把它当字符串处理。
📝 总结
-
精度陷阱:超过 16 位的数字用
Number存一定会丢精度,ID 会变异。 -
序列化坑:
BigInt无法被 JSON 序列化,会导致接口报错。 -
选型原则:
- 能用 Number 就用 Number(金额、计数、普通 ID)。
- 超长 ID 必须用 BigInt/String。
-
传输原则:后端计算可以用 BigInt,但给前端的数据,必须是 String。
以后再看到后端传回来的 ID 尾数不对,别犹豫,直接去检查是不是中间某一步被偷偷转成 Number 了!
觉得有用的话,点赞收藏防踩坑! 👍