大整數為什麼變成 9007199254740992
結論:超過 2^53-1 的整數在 IEEE 754 雙精度裡無法精確表示,JSON.parse 會靜默把末位改掉。訂單號、雪花 ID、金額都踩過這個坑,正確解法是全程當字串傳。

結論:把雪花演算法 ID、訂單號、bigint 主鍵這類識別碼當作字串,從序列化、傳輸到解析全程不進入數字通道;只有在確認欄位不會超過 2^53-1 時,才可以保留數字型別並在 Schema 裡加 maximum 約束。
因為解析它的那一刻,精度就已經丟了,而且不會報錯。
JSON.parse('{"id": 9007199254740993}');
// { id: 9007199254740992 }
少掉的那一位,找不回來了。
2^53 這條線怎麼來的
JavaScript 的 number 只有一種:IEEE 754 雙精度浮點,用 53 位表示有效數字(1 位隱含 + 52 位顯式),能精確表示的整數範圍是:
-9007199254740991 ~ 9007199254740991 (即 ±(2^53-1))
超過這條線,相鄰兩個整數之間的距離不再等於 1,而是 2、4、8…… 具體落到哪個值取決於二進位表示。9007199254740993 的末位是 1,而這個量級上能被表示的數末位都是 0,於是它只能進位到 9007199254740992。
哪些欄位最容易中招
| 欄位 | 典型位數 | 風險 |
|---|---|---|
| 雪花演算法 ID | 18–19 位 | 高,末幾位直接錯 |
| 訂單號、流水號 | 16–20 位 | 高 |
| 資料庫 bigint 主鍵 | 19 位 | 高 |
| 金額(以分為單位) | 一般安全 | 低 |
| 時間戳(毫秒) | 13 位 | 安全 |
共同點:它們是識別碼,不是拿來做算術的數字。識別碼只需要原樣搬運,所以最合適的型別是字串。
四種解法
一、序列化時就轉字串。 後端在出參前把 bigint 欄位轉成 string,前端按字串處理。最省事也最可靠,代價是要在介面文件寫清楚「這些欄位是字串」。
二、讓序列化函式庫處理。 Java 的 Jackson 可以給 Long 配 ToStringSerializer;Go 用 json:",string" 標籤。核心都是「不讓它進數字通道」。
三、前端用 BigInt 解析。 JSON.parse 本身不支援,需要 json-bigint 之類的函式庫。注意 BigInt 不能和 Number 混算,JSON.stringify 也不認它。
四、約定安全範圍。 如果欄位確實不會超過 2^53,在 Schema 裡加 maximum 約束,讓越界資料在校驗階段就失敗。
怎麼驗證自己有沒有踩坑
拿一個已知的 ID,走一遍「序列化 → 傳輸 → 解析」,比對前後是否逐字相等。抓包看原始回應體:如果回應裡是沒有引號的長數字,而前端拿到的末位不對,基本可以確定是解析階段丟的精度。
常見問題
為什麼 JSON.parse 改了末位卻不報錯?
JavaScript 的 number 只有 IEEE 754 雙精度一種型別,超出 2^53-1 的整數只能落到附近的近似值上。JSON.parse 把這一步當成正常的數值轉換,既不拋錯也不提示。
前端能否用 BigInt 保住大整數?
可以,但 JSON.parse 本身不支援,要換成 json-bigint 這類函式庫。代價是 BigInt 不能和 Number 混算,JSON.stringify 也不認它,後續處理要跟著改。
要不要在介面文件裡標明這些欄位是字串?
要。序列化時轉字串最省事也最可靠,但前提是消費方能按字串處理,所以欄位型別必須在介面文件裡寫死,否則前端仍會把它當數字用。