Loading [MathJax]/jax/output/CommonHTML/jax.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >JSON Bigint 大数精度丢失的背后

JSON Bigint 大数精度丢失的背后

原创
作者头像
猫哥学前班
修改于 2019-07-31 11:58:44
修改于 2019-07-31 11:58:44
15.5K0
举报
文章被收录于专栏:猫哥学前班猫哥学前班

如果你在 Chrome Dev Tools 控制台中输入 JSON.parse('{"taskid": 9007199254740993}') 运行结果返回的将会是 {taskid: 9007199254740992}。为什么 parse 后的数值会不一致?

双精度浮点数 IEEE 754

JavaScript 采用双精度浮点数( IEEE 754 标准)来表示它的 Number 类型。一个数字占用 64 bits 存储空间(这里的每一位都只能存放 0 或 1):

General double precision float
General double precision float

第一位 0 表示正值、1 表示负值;第 2- 12 位表示 2 的指数部分(可正可负);剩下的 52 个 bits 表示尾数部分,它的长度决定了数字的精度。

如果我们将符号位和指数位共 12 个 bits 表示为 16 进制(4 个二进制 bits 1111 得到 1 个 16 进制的 f),那么它的取值范围为 [000, 7ff]。其中,规范约定当取值 7ff 时,可以表示无穷大或 NaN。

所以双精度浮点数能表示的最大 16 进制数为 0x7fef_ffff_ffff_ffff,转为十进制约为 1.79 ×10 的 308 次方。能表示的数的范围非常大,但受限于尾数的长度,能“精确”表示的数字并不多,我们来看看这个数到底是多少。

最大安全整数

从以上表示公式我们能看到,当指数部分只取 1 位,尾数部分取满 52 位时,可以精确表示出 JavaScript 里的整数,其 16 进制形式为 0x001f_ffff_ffff_ffff ,即 9007199254740991

它等于 2 的 53 次方减 1,在 ES6 中,可以通过 Number.MAX_SAFE_INTEGER 引用到这个数值。

代码语言:txt
AI代码解释
复制
Number.MAX_SAFE_INTEGER === Math.pow(2, 53) - 1  // true
Number.MAX_SAFE_INTEGER === 0x001f_ffff_ffff_ffff   // true
Number.MAX_SAFE_INTEGER === 9007199254740991     // true
Number.MAX_SAFE_INTEGER === -Number.MIN_SAFE_INTEGER  // true

超过这个最大安全整数的运算,都可能因为发生进位溢出,造成精度丢失。

前后端大数传输方案

大数的运算和前后端传输是前端开发领域中的一个重要知识点。

本文开头提到的问题,源自于一个真实的项目案例,taskid 是 MySQL 数据库中的 bigint 类型字段。在 MySQL 中,一个 bigint 存储占用 8 Bytes 的空间,即 64 bits。当取值为无符号整型时,能表示的范围是 0 到 2 的 64 次方减 1,即 18446744073709551615

当 taskid 取值在 (9007199254740991, 18446744073709551615] 之间时,后端程序(受语言特性和第三方库影响)通常能正确的执行 JSON 序列化操作,并通过 HTTP 接口返回给前端,而前端执行 JSON.parse 解码时,会因为语言本身的限制发生精度丢失,引发 bug。

大数转字符串类型

为了解决大数传递精度丢失的问题,常见的方案是“将大数转为字符串类型”。具体的做法如下:

后端程序先将大数转为 string 类型,再进行 JSON encode,传给前端。前端拿到数据后 decode 成 string 类型,直接展示。当需要大数运算时,将 string split 成多段安全整数字符串,每段单独转为 number 类型,在安全范围内计算完成后,再 join 成 string 类型进行展示。

一些第三方库(如 json-bigint)之所以能正确的处理大数 parse ,且不造成精度丢失,其实现原理也是类似。在拿到接口的 JSON 数据时,并不直接 JSON.parse,而是先将整块数据当作 text 字符串,将其中的大数以 string 类型进行存储和标记,再使用定制化的 JSON.parse。

类型语义丢失

我们知道前端往后端 POST 数据时,有两种常见的编码形式 application/x-www-form-urlencodedapplication/json

当我们需要传递一个 number 类型的 id 给接口时,application/x-www-form-urlencoded 在 HTTP Request Body 中传输的是 id=1,而 application/json 的 Body 则是 {"id":1} 。我们之所以认为后者的语义更好,是因为后者能正确地反映出 id 的真实类型为 number。

而当这个 id 为 String 类型时,前者传输的依然是 id=1,后者则变为了 {"id":"1"}。对于后端程序来说,这层类型语义能让参数类型校验和计算更加准确和方便。

而如果前后端采用将“大数转为字符串”的方案,当 taskid 以 string 类型返回时,调用方将无法判断出它在业务和 DB 中到底是 char 字符类型存储的,还是 bigint 类型存储,导致类型语义丢失的情况发生。

类型语义有那么重要吗?这是另外一个话题了,但从 TypeScript 的发展趋势来看,为 JavaScript 加一个明确的类型,有很重大的意义。

ECMAScript 与 JSON 标准中的冲突

为了解决大数运算的问题,ECMAScript 标准中引入了 BigInt 类型(当前处于 Stage 3,且 Chrome 已经支持),通过在数字后面加一个 n,可以显式的声明一个 BigInt 类型对象,在进行运算时,将不再会发生精度丢失。

代码语言:txt
AI代码解释
复制
0x001f_ffff_ffff_ffffn + 2n === 9007199254740993n // true
2n**64n - 1n === 18446744073709551615n // true

在前端环境中,可以极其方便地进行大数运算。但这种做法,在进行 JSON 编解码时却遇到了大难题。

JSON 标准(IETF 7159)中定义了 JSON 支持的数据展示类型为 string、number、boolean、null 和这四个基础类型所组成的 object、array 结构。其他 JavaScript 类型在编解码时都会被静默消除或转换。

代码语言:txt
AI代码解释
复制
JSON.stringify({a:undefined, b: NaN, c: Symbol('c'), d:new Date(), e: new Set([1,2,3]), f:()=>{}}) 
// {"b":null,"d":"2019-07-31T10:21:47.848Z","e":{}}

从开发者的直观感受上,BigInt 作为 Number 类型的补充,应当在 JSON 标准中当作 Number 类型被支持。但从语言设计的角度来看,1 和 1n 是完全不同的对象类型,如果使用同一种表示方式,那么必然会发生“类型语义丢失”的现象。

更麻烦的地方在于,JSON 标准属于更广泛的标准,对 JSON 标准的改动,会影响到其他所有语言的实现,这可不是 JavaScript 弟弟能 hold 得住的。作为 ES 标准的制定者,TC39 委员会的大神们搁置了这个问题,而调皮的 Chrome 则在开发者试图 stringify 一个 BigInt 时,抛出了 Do not know how to serialize a BigInt 的异常。

事实上 JSON 标准中已经预料到,如果不设定 Number 的精度标准,可能会在不同系统传递数值时发生精度丢失的问题,所以也有建议开发者按照双精度浮点数规范来约束自己的系统。

如何利用 JavaScript BigInt 类型在不造成类型语义丢失的前提下,解决前后端接口大数的传输,是一个既有趣又有挑战的话题,同时也相当考验标准制定者和开发者的智慧了。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
​day019: 谈谈你对BigInt的理解。
这导致JS中的Number无法精确表示非常大的整数,它会将非常大的整数四舍五入,确切地说,JS中的Number类型只能安全地表示-9007199254740991(-(2^53-1))和9007199254740991((2^53-1)),任何超出此范围的整数值都可能失去精度。
用户3806669
2021/03/10
1.2K0
JavaScript 浮点数之迷:大数危机
在 JavaScript 中浮点数运算时经常出现 0.1+0.2=0.30000000000000004 这样的问题,除了这个问题之外还有一个不容忽视的大数危机(大数处理丢失精度问题),也是近期遇到的一些问题,做下梳理同时理解下背后产生的原因和解决方案。
五月君
2020/02/19
1.5K0
SpringBoot返回前端Long型丢失精度咋办
最近为Prong开发了一个基于snowflake算法的Java分布式ID组件,将实体主键从原来的String类型的UUID修改成了Long型的分布式ID。修改后发现前端显示的ID和数据库中的ID不一致。例如数据库中存储的是:812782555915911412,显示出来却成了812782555915911400,后面2位变成了0,精度丢失了:
用户3467126
2021/11/09
4.4K0
SpringBoot返回前端Long型丢失精度咋办
前端如何应对精确数字运算?用BigNumber.js解决JavaScript原生Number类型在处理大数或高精度计算时的局限性
BigNumber.js 是一个 JavaScript 库,用于处理高精度的数字运算。它解决了 JavaScript 原生 Number 类型在处理大数或高精度计算时的局限性。由于 JavaScript 的 Number 类型基于 IEEE 754 标准(双精度浮点数),其精度限制为 53 位有效数字,因此在处理大数或需要高精度的场景下容易出现精度丢失的问题。
watermelo37
2025/05/14
420
前端如何应对精确数字运算?用BigNumber.js解决JavaScript原生Number类型在处理大数或高精度计算时的局限性
一个函数让你看懂 'Why 0.1+0.2!=0.3'
由于 JavaScript中没有将小数的 二进制转换成 十进制的方法,于是手动实现了一个。
ConardLi
2019/05/23
6670
探秘 JavaScript 世界的神秘数字 1.7976931348623157e+308
JavaScript 的 Number 对象中存储了很多常量,神秘数字 1.7976931348623157e+308 就在其中,打开浏览器 Console,输入 Number.MAX_VALUE,就会得到这个数字:
清秋
2022/09/20
1.9K0
探秘 JavaScript 世界的神秘数字 1.7976931348623157e+308
JS最新基本数据类型:BigInt
BigInt数据类型的目的是比Number数据类型支持的范围更大的整数值。在对大整数执行数学运算时,以任意精度表示整数的能力尤为重要。使用BigInt,整数溢出将不再是问题。
前端小智@大迁世界
2019/07/31
2.8K0
长得太长也是错?——后端 Long 型 ID 精度丢失的“奇妙”修复之旅
在前后端分离的时代,我们的生活充满了无数的机遇与挑战——包括那些突然冒出来的让人抓狂的 Bug。今天我们要聊的,就是一个让无数开发者哭笑不得的经典问题:后端 Long 类型 ID 过长导致前端精度丢失。说到这个问题,那可真是“万恶之源”啊,谁让 JavaScript 只能安全地处理 Number.MAX_SAFE_INTEGER(也就是 9007199254740991)以内的数值呢?
繁依Fanyi
2024/08/15
1.6K0
JS面试题-js新增基本数据类型BigInt
BigInt 是一种内置对象,它提供了一种方法来表示大于 2的53次方 - 1 的整数。这原本是 Javascript 中可以用 Number 表示的最大数字。BigInt 可以表示任意大的整数。
用户10106350
2022/10/28
7660
JS面试题-js新增基本数据类型BigInt
preview和response的值不一样
前段时间在开发的过程中遇到一个奇怪的 Bug。 在服务端数据正常,前端页面渲染代码正常的情况下,浏览器页面渲染出的内容却不一样。 经过一番定位,最终在 Chrome 浏览器的控制台找到了线索。 在控制台里面查看到的情形是 response 和 preview 的值不一样。
epoos
2022/09/19
5.2K0
preview和response的值不一样
那些年遇到的刁钻JavaScript面试题(可防踩坑)
解析: 这是比较常规的面试题了,主要考察的是 JavaScript 中的隐式类型转换。在 JS 中 + 主要有两个作用:数字相加和字符串拼接,当 + 两边不都为数字时会把它们都转为字符串再拼接,所以第一个 2 会先被转成 '2' 再与第二个 '2' 拼接。
MudOnTire
2020/07/26
7270
[每日一题] JavaScript面试之“大数相加”运算
为什么会出现这个原因呢?先来探究一下Javascript的Number类型本质了,先来看看最权威的MDN对Javascript数字类型的定义。
用户1462769
2019/08/08
4.2K0
[每日一题]  JavaScript面试之“大数相加”运算
为什么不要在 JavaScript 中使用位操作符?
如果你的第一门编程语言不是 JavaScript,而是 C++ 或 Java,那么一开始你大概会看不惯 JavaScript 的数字类型。在 JavaScript 中的数字类型是不区分什么 Int,Float,Double,Decimal 的。咳咳,我说的当然是在 ES6 之前的 JS,在 ES6 的新标准中提出了像 Int8Array 这样新的数据类型。不过这不是本文叙述的重点,暂且就不谈啦。本文将更着重地谈 JS 的数字类型以及作用于它的位操作符,而关于包装对象 Number 的更多了解可以看拔赤翻译的
HTML5学堂
2018/03/12
1K0
为什么不要在 JavaScript 中使用位操作符?
ES11屡试不爽的新特性,你用上了几个?
严格限制一些用于内部使用的Class变量,只需要在变量前「#」,就可以使其成为私有变量,并且无法在class外部直接访问
Sneaker-前端公虾米
2021/12/23
5830
ES11屡试不爽的新特性,你用上了几个?
避坑 | 记一次前端长整数精度丢失问题
后端Java实现的接口如下,返回一个json格式的大整数 123456789123456789:
程序员鱼皮
2020/11/25
12K1
避坑 | 记一次前端长整数精度丢失问题
ES11屡试不爽的新特性,你用上了几个?
严格限制一些用于内部使用的Class变量,只需要在变量前添加#,就可以使其成为私有变量,并且无法在class外部直接访问
前端公虾米
2020/10/22
6820
ES11屡试不爽的新特性,你用上了几个?
JavaScript的数据类型
今天说说JavaScript的数据类型,很多人会认为有六种数据类型,其实不是很全面,我们就盘盘JavaScript到底有几种数据类型,我们分原始类型和引用类型说。
青年码农
2021/03/23
6670
面试官:聊聊 BigInt?
我们知道,现在 JavaScript 有 7 种基础类型,null/undefined/number/string/boolean/bigint/symbol。其中 bigint 是 ES2020 中正式加入的,有个别的面试官喜欢深挖这个特性,那么我们来了解一下 BigInt 到底是什么?我们为什么需要 BigInt 吧?
GopalFeng
2022/08/01
1.2K0
面试官:聊聊 BigInt?
踩坑记:当 JavaScript 遇上 UINT 64
作者:link 导语 写下这篇文章的缘由是因为在项目过程中,碰到了一个使用JavaScript处理 UINT64 类型数字的坑。 与大部分现代编程语言(包括几乎所有的脚本语言)一样,JavaScr
腾讯IVWEB团队
2017/04/12
4.8K0
种草 ES2020 新特性,真的学不动了
https://juejin.im/post/5e09ca40518825499a5abff7
coder_koala
2020/02/24
5350
推荐阅读
相关推荐
​day019: 谈谈你对BigInt的理解。
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档