后端的大数到 JS 端 JSON.parse 之后,经常大数溢出。这个问题之前一直是在后端看到一处,就 stringify 一处。现在实在是觉得烦了,想请教下各位有什么更好的做法?查了些资料,这个溢出是在 JS 进程里 JSON.parse 的时候发生的,跟 JS 自身有关系,跟 JSON 没有关系。想着,在 JSON.parse 的时候,换成 BigInt 的 JSON.parse 。但是这其中也有两种策略:1. 只要是整数,全部转成 BigInt ,不管实际会不会溢出。这样的好处是统一,坏处怕会有什么性能问题2. 只有当一个整数会溢出的时候,才会转成 BigInt 。但是这样用的时候还需要做下判断,比较麻烦。暂时想在内部后台系统里尝试下,浏览器兼容不成问题。有没有别的高招?PS. 这个真是 JS 的天坑。

感谢各位的关注与建议。下午实践了下:

在server端移除所有int(因为是python后端,所以直接称为int,不细分在db里是int还是bigint,下同)转string、string转回int的操作;
用json-bigint这个库,把所有的json里的int转成bigint。替换掉所有的JSON.parse与JSON.stringify。
重新生成所有model的TS定义,所有的int字段的类型从number替换成bigint。

整个过程还算平滑。遇到一些需要手动处理的问题:

一些第三方库只吃number,比如momentjs的 moment.unix(ts) 方法。在集成的地方,把这个方法替换成支持bigint的实现。
一些相关的工具函数,要改成bigint版本的。

因为只是后台系统,暂时没遇到什么性能问题,以后遇到了再来讨论想办法。这样改造后,溢出问题就不会污染到服务端实现,JS端也不用有特别的判断处理。

让后端返回字符串再处理?

返字符串,内部再转 Bignumber.js 处理

就是不想再在后端转字符串了呀!除非从 DB Access 层就把所有 bigint 都转成 string ,顺便好奇问下大家也是这么做的吗?像 timestamp 这种,在 db 里用 bigint 存储,但是使用的时候是实实在在要当数字使用的,如果转成 string 用的时候还要转回去。

这个很多语言都有类似的问题,最简单的还是让后端把可能溢出的字段用字符串类型传过来,前端自己转

如下,JSON 里整数是 64 位的,但到了 js v8 里却不支持 64 位整数,目前主流 cpu 都是 64 位,64 位整数最大值是~(1<<63) = (1<<63) -1 = 9223372036854775807:获得 json 格式的字符串$ php -r "var_dump(json_encode(9223372036854775807));"string(19) "9223372036854775807"$ php -r "var_dump(json_encode(9223372036854775808));"string(21) "9.223372036854776e+18"js 解析就丢失精度了,9223372036854775807 变成了 9223372036854776000 ,最后几位全变 0 了:JSON.parse("9223372036854775807")9223372036854776000

唉,基础知识不能叫做“天坑”吧,浮点数处理本来就有很多注意事项,编程语言是设计给“专家”使用的,本来就不是面向 end user 的。如果不是金融相关的,多数情况下都可以降低精度,如果确实需要很高精度,那也只好麻烦一点处理了。

之前也遇到这个问题,SQL 语句 select count() 查出来的数据默认就是 bigint 类型, 我是在后端直接做类型转换,将 bigint 转为 int 然后再返给前端的。前端解决的话可以看下这个 JS 库:json-bigint

我们也是后端处理成字符串返回的,如果一定要前端处理,可以试试利用第三方库在相应拦截中统一处理掉。

一些第三方库(如 json-bigint )之所以能正确的处理大数 parse ,且不造成精度丢失,其实现原理也是类似。在拿到接口的 JSON 数据时,并不直接 JSON.parse ,而是先将整块数据当作 text 字符串,将其中的大数以 string 类型进行存储和标记,再使用定制化的 JSON.parse 。自己处理的话,不外乎类似如此,可以单独抽取一个方法包裹 JSON.parse:javascriptvar text = '{ "name":"Bill Gates", "birth":"1955-10-28", "city":"Seattle"}';var obj = JSON.parse(text, function (key, value) { if (key == "birth") { return new Date(value); } else { return value; }});

转成 string 给前端,前端送回给后端的时候,后端得再转回 int ( python 后端),现在其实就是这么做的。就是时不时会遗漏掉,而且这种问题是要等溢出你才会发现。基于此,想寻找一个更好的方案。

我是后端,bigint 都是 parseString 给到前端的。

npm install --save bn.js

之前的方案是前端涉及数字都当 string 处理,后端也返回 string ,至于后端怎么处理可以避免忘记,不清楚后端怎么处理的,不知道 python 有没有前端 decimal.js 这种库,涉及数字全部用单独的库,而不是 原生 int ,也许有用?

后端框架不能直接指定大数类型序列化成 string 吗?只能一处处手动改?

这个和 JS 没有一毛钱关系,你要怪只能怪 ECMA 规范和 IEEE64 浮点数规范,还有 JSON 规范。

可以呀。问题是,不是所有的 bigint 转成 string 都能相安无事。比如,如果这个数字是用来做 ID 之类的,那它是 string 也无所谓,因为很少会对 ID 做什么加减乘除的运算。但是这个 bigint 可能是表示毫秒、表示钱,这时候转成 string 就很不方便。而且后端又不是只为 js 服务,还有 ios 跟 android 。

可以,但是最好不要这样子。一个后端+略微入门前端的人认为,一个语言不支持大数,是不太合理的。

json-bigint +1有现成的库了,不需要自己去处理,前后端都换用json-bigint就解决了

该说的楼上都说得差不多了,再加一个后端死活就不改,前端又不好用库的时候的一个骚操作吧:let jstr = '{"asd": 9223372036854775807}';console.info(JSON.parse(jstr));console.info(JSON.parse(jstr.replace(/\"asd\":[ ]?(-?[\d|.]+)/, (ma, p) => ma.replace(p, "${p}"))));console.info(JSON.parse('{"asd": -12345.6789}'.replace(/\"asd\":[ ]?(-?[\d|.]+)/, (ma, p) => ma.replace(p, "${p}"))));就是把 json 字符串先根据 key 把数字正则替换成字符串🐶为了骚而骚,别用,除非是为了给别人埋坑~

我是手动解析了后端请求,从网上找了 JSON.parse 的 polyfill ,改了实现,对超过精度的数字项改成字符串

Long 类型是吧, 其实我觉得很多项目 Long 当主键听没必要的。全局 Long 转 JSON 为 String 类型。

同意。不过遗留项目,已成定局。

你这样理解,js 里面数值只有 double 类型,double 自然是放不下 bigint 的数据的

请问下,如果这个数字要回传给后端你们怎么处理?也是让后端在 server 拿到后转成数字吗?

后端如果用得只是标准的 JSON 转换库的话,用 Long 类型自然就会出问题。

js 这个“缺陷”的原因我知道。话说,js 未来是否能支持真正的 int 类型,面量就写成 类似 97i32 或者 97i之类的?

如果你后端用的 golang 的话,可以在相应的结构体 json tag 中添加 ,string 来实现序列化 json 时将 int64 转为字符串。gotype T struct {ID int64 `json:id,string`}

传字符串给后端,后端可以转换的

在 VO 层做一下转 String ,其实成本也还好。

我都是序列化时把所有的数字转为字符串, 然后前端自己处理,这样就不会出现精度传着传着丢了的情况了

前端 axios 的话,可以自定义 parser ,然后用上面说的 json-bigint 。可以全局,也可以单个函数,用到的地方解析一下。typescript/** * 定义 parser */export function bigIntTransformer(data: string) { const jsonBig = jsonBigint({ storeAsString: true }); try { return jsonBig.parse(data); } catch { return JSON.parse(data); }}/** * 接口使用 */export function createAccountApi(reqVo: AccountReqVo) { return http.post(genApiUrl("/add"), reqVo, { transformRequest: bigIntTransformer, });}

得用第三方 json parser 库

为避免此类问题,无论前后端,我们在计算后都以字符串的形式传递,包括前后端之间的交互

先做出来再考虑性能问题吧

developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGERJS 里用的 Number 是由 ECMAScript 规范定义好的,使用双精度浮点型,而双精度浮点型是 IEEE 754-2019 定义的,有精度边界。用数字之前先看一下是不是超过了 MAX_SAFE_INTEGER 就行了(相应的还有 MIN_SAFE_INTEGER ),ES 和 JS 里面已经提供了这个常量可以用来对比。前端用 double 类型,后端也用 double 类型才算是合适;相应的后端如果用 int64 ,前端也得用 bigint 。使用其他语言也是一样的问题,就好比用 C 写的客户端使用 double 类型与用 int64 的后端通信。归根结底是数据类型一致可以直接避免所有问题。唯一的问题是 JSON 支持的数据类型有限,比如不支持 bigint ,所以就需要前后端换成其他兼容的类型(比如字符串)来使用 JSON 传输,或者干脆不用 JSON 换其他交换格式。JSON.parse()支持传入 reviver 函数来对 k/v 进行处理 developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#description当然也可以找一些现成的支持 bigint 类型的 json 序列化和反序列化库。

JS 最大安全整数 9007199254740991 ,9 千万亿,一般金额/毫秒什么的,应该是够用的。接口传字符串就行,提交时后端再转回整数应该也不麻烦吧,9 楼说得很明白。

JSON 最初设计的就是 JS 的子集,真要说谁的问题也是其他语言的问题,强行把超精度的数塞到 JSON 里了,甚至我见过后端框架虽然可以大数序列化不报错,但在序列化过程就已经产生精度问题了,前端拿到的就已经是错误的值

话说,js 未来是否能支持真正的 int 类型,面量就写成 类似 97i32 或者 97i之类的?不会,因为已经被 typedArray/wasm/asm.js 支持了,不可能再增加一种基本类型。

前端应该没有这种大数的计算操作吧?那就应该转为字符串,只是显示就好这个所谓的天坑也是 js 的性能优势之一

这个基本是初级工程师大概率会面对的问题,我们 C 段交互一般大概率是 string 了,要不然多端共用一套接口的话,指不定什么时候就出现各种千奇百怪的问题了

大兄弟秀优越也讲个基本法,当个谜语人,又讲不出什么东西。 淘宝订单都是先转字符串在处理的,这都是业界常识,跟基础知识有半毛钱关系?

不要折腾 int,就用 string

遇到大数不是第一时间用字符串或者自定义的格式传么?这要是天坑的话……那以后的路得多难走。

你如果在 v8 环境执行这个代码,就只能乖乖转字符串,如果后端不要字符串就走非 node 的 bff 再转一次

以前我是后端自己实现了一个 stringify 解决这种不同 JSON 解析器出现的奇怪问题的 - -

另外,纠正一下,溢出的是 JS Runtime ,不是 JSON 。在你把一个 int64 转成 JSON 格式的时候,它并没有“失真”,可以在其它支持 int64 的语言里试试。之前看 Twitter 的前端代码的时候,偶然发现他们有 id_str 这样的字段,今天翻了下文档,果然是为了处理大数溢出问题。 developer.twitter.com/en/docs/twitter-api/v1/data-dictionary/object-model/tweet

这其实是历史问题。当 Douglas Crockford 发明 JSON 的时候,其实只是为了 Javascript 方便用,就直接借用了 Javascript 定义 Object 的方式来制定 JSON 标准(所以 JSON 全称是 JavaScript Object Notation )。但他懒到根本没有按 Javascript 的 number 来定义 JSON 的 number 类型,JSON 的 number 在标准上并没有任何限制。在你的场景下你觉得 int64 的 JSON 数字在 JS 读不出正确精度是 bug ,但同样别的 app 也可以写一个 BigDecimal 转出来超过 int64 的大数,你的 app 同样不能正确读出来,也是 bug 。所以为了互操作性,不管在什么环境下 JSON 遇到数字的时候都应该当作 double 来处理,这样最不容易产生问题。ref: en.wikipedia.org/wiki/JSON -> interoperability