---
title: "两个订单 ID 在前端变成同一个：定位 JSON 大整数精度丢失"
date: 2026-11-13
slug: javascript-json-unsafe-integer
canonical: https://kernelranch.com/blog/2026/11/javascript-json-unsafe-integer
markdown_url: https://kernelranch.com/blog/2026/11/javascript-json-unsafe-integer.md
tags: ["javascript", "json", "identifiers"]
---后端返回两个相邻的 64 位订单 ID，前端列表只剩一条。先对照原始响应文本和 JSON.parse 后的值。如果 ID 超出 JavaScript 安全整数范围，两个值可能在进入列表之前就变成同一个 Number。

## 1. 观察精度从哪一步丢失

保存为 id_lab.mjs，运行 node id_lab.mjs。

```javascript
import assert from 'node:assert/strict';

const unsafe = JSON.parse('[9007199254740992,9007199254740993]');
assert.equal(unsafe[0], unsafe[1]);
assert.equal(Number.isSafeInteger(unsafe[1]), false);
assert.equal(new Map(unsafe.map(id => [id, true])).size, 1);
console.log('numeric JSON:', unsafe.join(', '));

const exact = JSON.parse('["9007199254740992","9007199254740993"]');
assert.equal(new Map(exact.map(id => [id, true])).size, 2);
assert.equal(BigInt(exact[1]) - BigInt(exact[0]), 1n);
assert.notEqual(BigInt(unsafe[1]), BigInt(exact[1]));
console.log('string JSON:', exact.join(', '));
console.log('distinct map keys: numeric=1, string=2');
```

本机实测输出如下。

```text
numeric JSON: 9007199254740992, 9007199254740992
string JSON: 9007199254740992, 9007199254740993
distinct map keys: numeric=1, string=2
```

## 2. 从生产者开始修复字段契约

服务端先从数据库驱动获得完整整数，再以十进制字符串发送 ID。如果驱动已经把值转成不安全的 Number，后续 String 或 BigInt 都只能处理舍入后的值。排查时依次检查数据库结果、序列化前的类型、HTTP 原文、以及浏览器解析结果。

| 字段 | 接口约定 |
|---|---|
| 订单 ID | 十进制字符串，客户端按字符串传回和去重 |
| 普通计数 | 设定上下界，保留在消费者可精确处理的范围内 |
| 需要精确运算的整数 | 从字符串构造 BigInt，按接口约定转回字符串 |

给已有 API 改类型时，可以先增加字符串字段，迁移所有消费者后再处理旧字段。契约测试至少覆盖最大安全整数、紧邻边界的两个值和真实数据库可返回的最大 ID。

## 小结

发现表格键重复时先看原始 JSON。保留完整 ID 需要生产者、传输格式和消费端都遵守同一类型契约。

## 数据来源与检索时间

检索时间 2026-11-13。

| 来源 | URL | 对应内容 |
|---|---|---|
| RFC 8259 §6 | https://www.rfc-editor.org/rfc/rfc8259.html#section-6 | JSON 数字与跨实现精确互操作范围 |
| ECMAScript | https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-number.issafeinteger | 安全整数的定义 |

## 核验说明

- Node.js v24.19.0 验证原文解析、Map 键冲突以及 BigInt 从字符串转换。
