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 · 192 天前 · 3219 次点击
    这是一个创建于 192 天前的主题,其中的信息可能已经有所发展或是发生改变。
    后端的大数到 JS 端 JSON.parse 之后,经常大数溢出。

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

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

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

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

    PS. 这个真是 JS 的天坑。
    第 1 条附言  ·  192 天前

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

    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 条附言  ·  83 天前
    之前刚切换的时候,页面还很少。经过一段时间的实践,发现用 bigint 的坑也很大:

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

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

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

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

    前端解决的话可以看下这个 JS 库:json-bigint
    ZAnko
        8
    ZAnko  
       192 天前
    我们也是后端处理成字符串返回的,如果一定要前端处理,可以试试利用第三方库在相应拦截中统一处理掉。
    iOCZ
        9
    iOCZ  
       192 天前
    一些第三方库(如 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
       192 天前
    转成 string 给前端,前端送回给后端的时候,后端得再转回 int ( python 后端),现在其实就是这么做的。就是时不时会遗漏掉,而且这种问题是要等溢出你才会发现。基于此,想寻找一个更好的方案。
    Morii
        11
    Morii  
       192 天前
    我是后端,bigint 都是 parseString 给到前端的。
    Terry166
        12
    Terry166  
       192 天前 via iPhone
    npm install --save bn.js
    webbillion
        13
    webbillion  
       192 天前
    之前的方案是前端涉及数字都当 string 处理,后端也返回 string ,至于后端怎么处理可以避免忘记,不清楚后端怎么处理的,不知道 python 有没有前端 decimal.js 这种库,涉及数字全部用单独的库,而不是 原生 int ,也许有用?
    mxT52CRuqR6o5
        14
    mxT52CRuqR6o5  
       192 天前
    后端框架不能直接指定大数类型序列化成 string 吗?只能一处处手动改?
    thinkershare
        15
    thinkershare  
       192 天前
    这个和 JS 没有一毛钱关系,你要怪只能怪 ECMA 规范和 IEEE64 浮点数规范,还有 JSON 规范。
    justdoit123
        16
    justdoit123  
    OP
       192 天前
    @mxT52CRuqR6o5 可以呀。

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

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

    但是这个 bigint 可能是表示毫秒、表示钱,这时候转成 string 就很不方便。而且后端又不是只为 js 服务,还有 ios 跟 android 。
    RedBeanIce
        17
    RedBeanIce  
       192 天前
    @mxT52CRuqR6o5

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

    一个后端+略微入门前端的人认为,一个语言不支持大数,是不太合理的。
    clue
        18
    clue  
       192 天前
    json-bigint +1

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

    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  
       192 天前
    我是手动解析了后端请求,从网上找了 JSON.parse 的 polyfill ,改了实现,对超过精度的数字项改成字符串
    coala
        21
    coala  
       192 天前
    Long 类型是吧, 其实我觉得很多项目 Long 当主键听没必要的。

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

    话说,js 未来是否能支持真正的 int 类型,面量就写成 类似 `97i32` 或者 `97i`之类的?
    Martens
        27
    Martens  
       192 天前
    如果你后端用的 golang 的话,可以在相应的结构体 json tag 中添加 ,string 来实现序列化 json 时将 int64 转为字符串。
    ```go
    type T struct {
    ID int64 `json:id,string`
    }
    ```
    jazzg62
        28
    jazzg62  
       192 天前
    @justdoit123 传字符串给后端,后端可以转换的
    iMouseWu
        29
    iMouseWu  
       192 天前
    @justdoit123 在 VO 层做一下转 String ,其实成本也还好。
    fiveStarLaoliang
        30
    fiveStarLaoliang  
       192 天前
    我都是序列化时把所有的数字转为字符串, 然后前端自己处理,这样就不会出现精度传着传着丢了的情况了
    wusheng0
        31
    wusheng0  
       192 天前
    前端 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  
       192 天前 via Android
    得用第三方 json parser 库
    humbass
        33
    humbass  
       192 天前
    为避免此类问题,无论前后端,我们在计算后都以字符串的形式传递,包括前后端之间的交互
    darkengine
        34
    darkengine  
       192 天前
    先做出来再考虑性能问题吧
    libook
        35
    libook  
       192 天前
    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  
       192 天前
    JS 最大安全整数 9007199254740991 ,9 千万亿,一般金额/毫秒什么的,应该是够用的。接口传字符串就行,提交时后端再转回整数应该也不麻烦吧,9 楼说得很明白。
    mxT52CRuqR6o5
        37
    mxT52CRuqR6o5  
       192 天前
    JSON 最初设计的就是 JS 的子集,真要说谁的问题也是其他语言的问题,强行把超精度的数塞到 JSON 里了,甚至我见过后端框架虽然可以大数序列化不报错,但在序列化过程就已经产生精度问题了,前端拿到的就已经是错误的值
    RedNax
        38
    RedNax  
       192 天前
    @justdoit123
    > 话说,js 未来是否能支持真正的 int 类型,面量就写成 类似 `97i32` 或者 `97i`之类的?

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

    这个所谓的天坑也是 js 的性能优势之一
    mysunshinedreams
        40
    mysunshinedreams  
       192 天前
    这个基本是初级工程师大概率会面对的问题,我们 C 段交互一般大概率是 string 了,要不然多端共用一套接口的话,指不定什么时候就出现各种千奇百怪的问题了
    nianyu
        41
    nianyu  
       192 天前
    @cmdOptionKana 大兄弟秀优越也讲个基本法,当个谜语人,又讲不出什么东西。 淘宝订单都是先转字符串在处理的,这都是业界常识,跟基础知识有半毛钱关系?
    palytoxin
        42
    palytoxin  
       192 天前
    不要折腾 int,就用 string
    IvanLi127
        43
    IvanLi127  
       192 天前
    遇到大数不是第一时间用字符串或者自定义的格式传么?这要是天坑的话……那以后的路得多难走。
    BurNIng1988
        44
    BurNIng1988  
       192 天前
    你如果在 v8 环境执行这个代码,就只能乖乖转字符串,如果后端不要字符串就走非 node 的 bff 再转一次
    qeqv
        45
    qeqv  
       192 天前
    以前我是后端自己实现了一个 stringify 解决这种不同 JSON 解析器出现的奇怪问题的 - -
    justdoit123
        46
    justdoit123  
    OP
       192 天前
    另外,纠正一下,溢出的是 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  
       191 天前 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   ·   我们的愿景   ·   实用小工具   ·   876 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 28ms · UTC 21:57 · PVG 05:57 · LAX 14:57 · JFK 17:57
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.