前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JSON Bigint 大数精度丢失的背后

JSON Bigint 大数精度丢失的背后

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

如果你在 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 表示尾数部分,它的长度决定了数字的精度。

(-1)^{sign} × 2^{exponent-0x3ff} × 1.mantissa

如果我们将符号位和指数位共 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
复制
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
复制
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
复制
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 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 双精度浮点数 IEEE 754
  • 最大安全整数
  • 前后端大数传输方案
  • 大数转字符串类型
  • 类型语义丢失
  • ECMAScript 与 JSON 标准中的冲突
相关产品与服务
云数据库 SQL Server
腾讯云数据库 SQL Server (TencentDB for SQL Server)是业界最常用的商用数据库之一,对基于 Windows 架构的应用程序具有完美的支持。TencentDB for SQL Server 拥有微软正版授权,可持续为用户提供最新的功能,避免未授权使用软件的风险。具有即开即用、稳定可靠、安全运行、弹性扩缩等特点。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档