在 JavaScript 中浮点数运算时常常出现 0.1+0.2=0.30000000000000004 这样的问题,除了这个问题以外还有一个不容忽视的大数危机(大数处理丢失精度问题),也是近期遇到的一些问题,作下梳理同时理解下背后产生的缘由和解决方案。html
做者简介:五月君,Nodejs Developer,慕课网认证做者,热爱技术、喜欢分享的 90 后青年,欢迎关注 Nodejs技术栈 和 Github 开源项目 www.nodejs.red前端
在开始本节以前,但愿你能事先了解一些 JavaScript 浮点数的相关知识,在上篇文章 JavaScript 浮点数之迷:0.1 + 0.2 为何不等于 0.3?中很好的介绍了浮点数的存储原理、为何会产生精度丢失(建议事先阅读下)。node
IEEE 754 双精确度浮点数(Double 64 Bits)中尾数部分是用来存储整数的有效位数,为 52 位,加上省略的一位 1 能够保存的实际数值为 。git
Math.pow(2, 53) // 9007199254740992
Number.MAX_SAFE_INTEGER // 最大安全整数 9007199254740991
Number.MIN_SAFE_INTEGER // 最小安全整数 -9007199254740991
复制代码
只要不超过 JavaScript 中最大安全整数和最小安全整数范围都是安全的。github
例一编程
当你在 Chrome 的控制台或者 Node.js 运行环境里执行如下代码后会出现如下结果,What?为何我定义的 200000436035958034 却被转义为了 200000436035958050,在了解了 JavaScript 浮点数存储原理以后,应该明白此时已经触发了 JavaScript 的最大安全整数范围。json
const num = 200000436035958034;
console.log(num); // 200000436035958050
复制代码
例二后端
如下示例经过流读取传递的数据,保存在一个字符串 data 中,由于传递的是一个 application/json 协议的数据,咱们须要对 data 反序列化为一个 obj 作业务处理。安全
const http = require('http');
http.createServer((req, res) => {
if (req.method === 'POST') {
let data = '';
req.on('data', chunk => {
data += chunk;
});
req.on('end', () => {
console.log('未 JSON 反序列化状况:', data);
try {
// 反序列化为 obj 对象,用来处理业务
const obj = JSON.parse(data);
console.log('通过 JSON 反序列化以后:', obj);
res.setHeader("Content-Type", "application/json");
res.end(data);
} catch(e) {
console.error(e);
res.statusCode = 400;
res.end("Invalid JSON");
}
});
} else {
res.end('OK');
}
}).listen(3000)
复制代码
运行上述程序以后在 POSTMAN 调用,200000436035958034 这个是一个大数值。app
如下为输出结果,发现没有通过 JSON 序列化的一切正常,当程序执行 JSON.parse() 以后,又发生了精度问题,这又是为何呢?JSON 转换和大数值精度之间又有什么猫腻呢?
未 JSON 反序列化状况: {
"id": 200000436035958034
}
通过 JSON 反序列化以后: { id: 200000436035958050 }
复制代码
这个问题也实际遇到过,发生的方式是调用第三方接口拿到的是一个大数值的参数,结果 JSON 以后就出现了相似问题,下面作下分析。
先了解下 JSON 的数据格式标准,Internet Engineering Task Force 7159,简称(IETF 7159),是一种轻量级的、基于文本与语言无关的数据交互格式,源自 ECMAScript 编程语言标准.
www.rfc-editor.org/rfc/rfc7159… 访问这个地址查看协议的相关内容。
咱们本节须要关注的是 “一个 JSON 的 Value 是什么呢?” 上述协议中有规定必须为 object, array, number, or string 四个数据类型,也能够是 false, null, true 这三个值。
到此,也就揭开了这个谜底,JSON 在解析时对于其它类型的编码都会被默认转换掉。对应咱们这个例子中的大数值会默认编码为 number 类型,这也是形成精度丢失的真正缘由。
在先后端交互中这是一般的一种方案,例如,对订单号的存储采用数值类型 Java 中的 long 类型表示的最大值为 2 的 64 次方,而 JS 中为 Number.MAX_SAFE_INTEGER (Math.pow(2, 53) - 1),显然超过 JS 中能表示的最大安全值以外就要丢失精度了,最好的解法就是将订单号由数值型转为字符串返回给前端处理,这是再和一个供应商对接过程当中实实在在遇到的一个坑。
Bigint 是 JavaScript 中一个新的数据类型,能够用来操做超出 Number 最大安全范围的整数。
建立 BigInt 方法一
一种方法是在数字后面加上数字 n
200000436035958034n; // 200000436035958034n
复制代码
建立 BigInt 方法二
另外一种方法是使用构造函数 BigInt(),还须要注意的是使用 BigInt 时最好仍是使用字符串,不然仍是会出现精度问题,看官方文档也提到了这块 github.com/tc39/propos… 称为疑难杂症
BigInt('200000436035958034') // 200000436035958034n
// 注意要使用字符串不然仍是会被转义
BigInt(200000436035958034) // 200000436035958048n 这不是一个正确的结果
复制代码
检测类型
BigInt 是一个新的数据类型,所以它与 Number 并非彻底相等的,例如 1n 将不会全等于 1。
typeof 200000436035958034n // bigint
1n === 1 // false
复制代码
运算
BitInt 支持常见的运算符,可是永远不要与 Number 混合使用,请始终保持一致。
// 正确
200000436035958034n + 1n // 200000436035958035n
// 错误
200000436035958034n + 1
^
TypeError: Cannot mix BigInt and other types, use explicit conversions
复制代码
BigInt 转为字符串
String(200000436035958034n) // 200000436035958034
// 或者如下方式
(200000436035958034n).toString() // 200000436035958034
复制代码
与 JSON 的冲突
使用 JSON.parse('{"id": 200000436035958034}') 来解析会形成精度丢失问题,既然如今有了一个 BigInt 出现,是否使用如下方式就能够正常解析呢?
JSON.parse('{"id": 200000436035958034n}');
复制代码
运行以上程序以后,会获得一个 SyntaxError: Unexpected token n in JSON at position 25
错误,最麻烦的就在这里,由于 JSON 是一个更为普遍的数据协议类型,影响面很是普遍,不是轻易可以变更的。
在 TC39 proposal-bigint 仓库中也有人提过这个问题 github.comtc39/proposal-bi… 截至目前,该提案并未被添加到 JSON 中,由于这将破坏 JSON 的格式,极可能致使没法解析。
BigInt 的支持
BigInt 提案目前已进入 Stage 4,已经在 Chrome,Node,Firefox,Babel 中发布,在 Node.js 中支持的版本为 12+。
BigInt 总结
咱们使用 BigInt 作一些运算是没有问题的,可是和第三方接口交互,若是对 JSON 字符串作序列化遇到一些大数问题仍是会出现精度丢失,显然这是因为与 JSON 的冲突致使的,下面给出第三种方案。
经过一些第三方库也能够解决,可是你可能会想为何要这么曲折呢?转成字符串你们不都开开心心的吗,可是呢,有的时候你须要对接第三方接口,取到的数据就包含这种大数的状况,且遇到那种拒不改的,业务总归要完成吧!这里介绍第三种实现方案。
还拿咱们上面 大数处理精度丢失问题复现 的第二个例子进行讲解,经过 json-bigint 这个库来解决。
知道了 JSON 规范与 JavaScript 之间的冲突问题以后,就不要直接使用 JSON.parse() 了,在接收数据流以后,先经过字符串方式进行解析,利用 json-bigint 这个库,会自动的将超过 2 的 53 次方类型的数值转为一个 BigInt 类型,再设置一个参数 storeAsString: true
会将 BigInt 自动转为字符串。
const http = require('http');
const JSONbig = require('json-bigint')({ 'storeAsString': true});
http.createServer((req, res) => {
if (req.method === 'POST') {
let data = '';
req.on('data', chunk => {
data += chunk;
});
req.on('end', () => {
try {
// 使用第三方库进行 JSON 序列化
const obj = JSONbig.parse(data)
console.log('通过 JSON 反序列化以后:', obj);
res.setHeader("Content-Type", "application/json");
res.end(data);
} catch(e) {
console.error(e);
res.statusCode = 400;
res.end("Invalid JSON");
}
});
} else {
res.end('OK');
}
}).listen(3000)
复制代码
再次验证会看到如下结果,此次是正确的,问题也已经完美解决了!
JSON 反序列化以后 id 值: { id: '200000436035958034' }
复制代码
本文提出了一些产生大数精度丢失的缘由,同时又给出了几种解决方案,如遇到相似问题,均可参考。仍是建议你们在系统设计时去遵循双精度浮点数的规范来作,在查找问题的过程当中,有看到有些使用正则来匹配,我的角度仍是不推荐的,一是正则自己就是一个耗时的操做,二操做起来还要查找一些匹配规律,一不当心可能会把返回结果中的全部数值都转为字符串,也是不可行的。
v8.dev/features/bi… github.com/tc39/propos… en.wikipedia.org/wiki/Double…