V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐关注
Meteor
JSLint - a JavaScript code quality tool
jsFiddle
D3.js
WebStorm
推荐书目
JavaScript 权威指南第 5 版
Closure: The Definitive Guide
justdoit123
V2EX  ›  JavaScript

JS 大数溢出问题

  •  
  •   justdoit123 · 2023-10-18 10:21:24 +08:00 · 3744 次点击
    这是一个创建于 400 天前的主题,其中的信息可能已经有所发展或是发生改变。
    后端的大数到 JS 端 JSON.parse 之后,经常大数溢出。

    这个问题之前一直是在后端看到一处,就 stringify 一处。现在实在是觉得烦了,想请教下各位有什么更好的做法?

    查了些资料,这个溢出是在 JS 进程里 JSON.parse 的时候发生的,跟 JS 自身有关系,跟 JSON 没有关系。想着,在 JSON.parse 的时候,换成 BigInt 的 JSON.parse 。但是这其中也有两种策略:

    1. 只要是整数,全部转成 BigInt ,不管实际会不会溢出。这样的好处是统一,坏处怕会有什么性能问题
    2. 只有当一个整数会溢出的时候,才会转成 BigInt 。但是这样用的时候还需要做下判断,比较麻烦。

    暂时想在内部后台系统里尝试下,浏览器兼容不成问题。有没有别的高招?

    PS. 这个真是 JS 的天坑。
    第 1 条附言  ·  2023-10-18 18:25:18 +08:00

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

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

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

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

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

    第 2 条附言  ·  290 天前
    之前刚切换的时候,页面还很少。经过一段时间的实践,发现用 bigint 的坑也很大:

    很多第三方库不支持 bigint ,一旦没支持用上 bigint 就会报错。一两个还好,调用简单也还好。要命的是几乎每个库都不支持,而有些库的调用入口很多,不像 momentjs 这样那么好 hook 。

    所以,一开始是不想在 server 端缝缝补补。这下好了,到前端也是要各种缝缝补补,而且还更难缝补。
    47 条回复    2023-10-19 12:28:58 +08:00
    realJamespond
        1
    realJamespond  
       2023-10-18 10:23:26 +08:00
    让后端返回字符串再处理?
    neotheone2333
        2
    neotheone2333  
       2023-10-18 10:27:06 +08:00
    返字符串,内部再转 Bignumber.js 处理
    justdoit123
        3
    justdoit123  
    OP
       2023-10-18 10:27:14 +08:00
    @realJamespond 就是不想再在后端转字符串了呀!

    除非从 DB Access 层就把所有 bigint 都转成 string ,顺便好奇问下大家也是这么做的吗?像 timestamp 这种,在 db 里用 bigint 存储,但是使用的时候是实实在在要当数字使用的,如果转成 string 用的时候还要转回去。
    debuggerx
        4
    debuggerx  
       2023-10-18 10:27:40 +08:00
    这个很多语言都有类似的问题,最简单的还是让后端把可能溢出的字段用字符串类型传过来,前端自己转
    zjsxwc
        5
    zjsxwc  
       2023-10-18 10:31:28 +08:00
    如下,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
    ```
    cmdOptionKana
        6
    cmdOptionKana  
       2023-10-18 10:32:02 +08:00
    唉,基础知识不能叫做“天坑”吧,浮点数处理本来就有很多注意事项,编程语言是设计给“专家”使用的,本来就不是面向 end user 的。

    如果不是金融相关的,多数情况下都可以降低精度,如果确实需要很高精度,那也只好麻烦一点处理了。
    icoomn
        7
    icoomn  
       2023-10-18 10:32:28 +08:00
    之前也遇到这个问题,SQL 语句 select count() 查出来的数据默认就是 bigint 类型, 我是在后端直接做类型转换,将 bigint 转为 int 然后再返给前端的。

    前端解决的话可以看下这个 JS 库:json-bigint
    ZAnko
        8
    ZAnko  
       2023-10-18 10:35:41 +08:00
    我们也是后端处理成字符串返回的,如果一定要前端处理,可以试试利用第三方库在相应拦截中统一处理掉。
    iOCZ
        9
    iOCZ  
       2023-10-18 10:36:14 +08:00
    一些第三方库(如 json-bigint )之所以能正确的处理大数 parse ,且不造成精度丢失,其实现原理也是类似。在拿到接口的 JSON 数据时,并不直接 JSON.parse ,而是先将整块数据当作 text 字符串,将其中的大数以 string 类型进行存储和标记,再使用定制化的 JSON.parse 。

    自己处理的话,不外乎类似如此,可以单独抽取一个方法包裹 JSON.parse:
    ```javascript
    var 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;
    }});
    ```
    justdoit123
        10
    justdoit123  
    OP
       2023-10-18 10:37:48 +08:00
    转成 string 给前端,前端送回给后端的时候,后端得再转回 int ( python 后端),现在其实就是这么做的。就是时不时会遗漏掉,而且这种问题是要等溢出你才会发现。基于此,想寻找一个更好的方案。
    28Sv0ngQfIE7Yloe
        11
    28Sv0ngQfIE7Yloe  
       2023-10-18 10:47:11 +08:00
    我是后端,bigint 都是 parseString 给到前端的。
    Terry166
        12
    Terry166  
       2023-10-18 10:48:25 +08:00 via iPhone
    npm install --save bn.js
    webbillion
        13
    webbillion  
       2023-10-18 10:49:32 +08:00
    之前的方案是前端涉及数字都当 string 处理,后端也返回 string ,至于后端怎么处理可以避免忘记,不清楚后端怎么处理的,不知道 python 有没有前端 decimal.js 这种库,涉及数字全部用单独的库,而不是 原生 int ,也许有用?
    mxT52CRuqR6o5
        14
    mxT52CRuqR6o5  
       2023-10-18 10:52:42 +08:00
    后端框架不能直接指定大数类型序列化成 string 吗?只能一处处手动改?
    thinkershare
        15
    thinkershare  
       2023-10-18 10:56:49 +08:00
    这个和 JS 没有一毛钱关系,你要怪只能怪 ECMA 规范和 IEEE64 浮点数规范,还有 JSON 规范。
    justdoit123
        16
    justdoit123  
    OP
       2023-10-18 10:57:29 +08:00
    @mxT52CRuqR6o5 可以呀。

    问题是,不是所有的 bigint 转成 string 都能相安无事。

    比如,如果这个数字是用来做 ID 之类的,那它是 string 也无所谓,因为很少会对 ID 做什么加减乘除的运算。

    但是这个 bigint 可能是表示毫秒、表示钱,这时候转成 string 就很不方便。而且后端又不是只为 js 服务,还有 ios 跟 android 。
    RedBeanIce
        17
    RedBeanIce  
       2023-10-18 11:00:45 +08:00
    @mxT52CRuqR6o5

    可以,但是最好不要这样子。

    一个后端+略微入门前端的人认为,一个语言不支持大数,是不太合理的。
    clue
        18
    clue  
       2023-10-18 11:01:00 +08:00
    json-bigint +1

    有现成的库了,不需要自己去处理,前后端都换用`json-bigint`就解决了
    debuggerx
        19
    debuggerx  
       2023-10-18 11:01:06 +08:00
    该说的楼上都说得差不多了,再加一个后端死活就不改,前端又不好用库的时候的一个骚操作吧:

    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 把数字正则替换成字符串🐶

    为了骚而骚,别用,除非是为了给别人埋坑~
    jazzg62
        20
    jazzg62  
       2023-10-18 11:01:54 +08:00
    我是手动解析了后端请求,从网上找了 JSON.parse 的 polyfill ,改了实现,对超过精度的数字项改成字符串
    coala
        21
    coala  
       2023-10-18 11:09:48 +08:00
    Long 类型是吧, 其实我觉得很多项目 Long 当主键听没必要的。

    全局 Long 转 JSON 为 String 类型。
    justdoit123
        22
    justdoit123  
    OP
       2023-10-18 11:13:08 +08:00
    @coala 同意。不过遗留项目,已成定局。
    cheng6563
        23
    cheng6563  
       2023-10-18 11:14:17 +08:00
    你这样理解,js 里面数值只有 double 类型,double 自然是放不下 bigint 的数据的
    justdoit123
        24
    justdoit123  
    OP
       2023-10-18 11:16:38 +08:00
    @jazzg62 请问下,如果这个数字要回传给后端你们怎么处理?也是让后端在 server 拿到后转成数字吗?
    cheng6563
        25
    cheng6563  
       2023-10-18 11:18:28 +08:00
    后端如果用得只是标准的 JSON 转换库的话,用 Long 类型自然就会出问题。
    justdoit123
        26
    justdoit123  
    OP
       2023-10-18 11:19:35 +08:00
    js 这个“缺陷”的原因我知道。

    话说,js 未来是否能支持真正的 int 类型,面量就写成 类似 `97i32` 或者 `97i`之类的?
    Martens
        27
    Martens  
       2023-10-18 11:26:18 +08:00
    如果你后端用的 golang 的话,可以在相应的结构体 json tag 中添加 ,string 来实现序列化 json 时将 int64 转为字符串。
    ```go
    type T struct {
    ID int64 `json:id,string`
    }
    ```
    jazzg62
        28
    jazzg62  
       2023-10-18 11:29:32 +08:00
    @justdoit123 传字符串给后端,后端可以转换的
    iMouseWu
        29
    iMouseWu  
       2023-10-18 11:37:40 +08:00
    @justdoit123 在 VO 层做一下转 String ,其实成本也还好。
    fiveStarLaoliang
        30
    fiveStarLaoliang  
       2023-10-18 11:59:28 +08:00
    我都是序列化时把所有的数字转为字符串, 然后前端自己处理,这样就不会出现精度传着传着丢了的情况了
    wusheng0
        31
    wusheng0  
       2023-10-18 12:03:24 +08:00
    前端 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,
    });
    }
    ```
    zhy0216
        32
    zhy0216  
       2023-10-18 12:22:00 +08:00 via Android
    得用第三方 json parser 库
    humbass
        33
    humbass  
       2023-10-18 12:48:49 +08:00
    为避免此类问题,无论前后端,我们在计算后都以字符串的形式传递,包括前后端之间的交互
    darkengine
        34
    darkengine  
       2023-10-18 12:55:29 +08:00
    先做出来再考虑性能问题吧
    libook
        35
    libook  
       2023-10-18 14:25:27 +08:00
    https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER

    JS 里用的 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 进行处理 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#description
    当然也可以找一些现成的支持 bigint 类型的 json 序列化和反序列化库。
    hesetiema
        36
    hesetiema  
       2023-10-18 14:41:44 +08:00
    JS 最大安全整数 9007199254740991 ,9 千万亿,一般金额/毫秒什么的,应该是够用的。接口传字符串就行,提交时后端再转回整数应该也不麻烦吧,9 楼说得很明白。
    mxT52CRuqR6o5
        37
    mxT52CRuqR6o5  
       2023-10-18 14:48:04 +08:00
    JSON 最初设计的就是 JS 的子集,真要说谁的问题也是其他语言的问题,强行把超精度的数塞到 JSON 里了,甚至我见过后端框架虽然可以大数序列化不报错,但在序列化过程就已经产生精度问题了,前端拿到的就已经是错误的值
    RedNax
        38
    RedNax  
       2023-10-18 15:15:23 +08:00
    @justdoit123
    > 话说,js 未来是否能支持真正的 int 类型,面量就写成 类似 `97i32` 或者 `97i`之类的?

    不会,因为已经被 typedArray/wasm/asm.js 支持了,不可能再增加一种基本类型。
    Huelse
        39
    Huelse  
       2023-10-18 15:18:50 +08:00
    前端应该没有这种大数的计算操作吧?那就应该转为字符串,只是显示就好

    这个所谓的天坑也是 js 的性能优势之一
    mysunshinedreams
        40
    mysunshinedreams  
       2023-10-18 16:10:25 +08:00
    这个基本是初级工程师大概率会面对的问题,我们 C 段交互一般大概率是 string 了,要不然多端共用一套接口的话,指不定什么时候就出现各种千奇百怪的问题了
    nianyu
        41
    nianyu  
       2023-10-18 16:13:27 +08:00
    @cmdOptionKana 大兄弟秀优越也讲个基本法,当个谜语人,又讲不出什么东西。 淘宝订单都是先转字符串在处理的,这都是业界常识,跟基础知识有半毛钱关系?
    palytoxin
        42
    palytoxin  
       2023-10-18 17:19:06 +08:00
    不要折腾 int,就用 string
    IvanLi127
        43
    IvanLi127  
       2023-10-18 17:26:56 +08:00
    遇到大数不是第一时间用字符串或者自定义的格式传么?这要是天坑的话……那以后的路得多难走。
    BurNIng1988
        44
    BurNIng1988  
       2023-10-18 18:02:22 +08:00
    你如果在 v8 环境执行这个代码,就只能乖乖转字符串,如果后端不要字符串就走非 node 的 bff 再转一次
    qeqv
        45
    qeqv  
       2023-10-18 18:38:23 +08:00
    以前我是后端自己实现了一个 stringify 解决这种不同 JSON 解析器出现的奇怪问题的 - -
    justdoit123
        46
    justdoit123  
    OP
       2023-10-18 18:55:45 +08:00
    另外,纠正一下,溢出的是 JS Runtime ,不是 JSON 。在你把一个 int64 转成 JSON 格式的时候,它并没有“失真”,可以在其它支持 int64 的语言里试试。

    之前看 Twitter 的前端代码的时候,偶然发现他们有 `id_str` 这样的字段,今天翻了下文档,果然是为了处理大数溢出问题。https://developer.twitter.com/en/docs/twitter-api/v1/data-dictionary/object-model/tweet
    YuJianrong
        47
    YuJianrong  
       2023-10-19 12:28:58 +08:00 via iPad
    @justdoit123 这其实是历史问题。
    当 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: https://en.wikipedia.org/wiki/JSON -> interoperability
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3309 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 11:57 · PVG 19:57 · LAX 03:57 · JFK 06:57
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.