0.1+0.2=0.30000000000000004的问题在JavaScript浮点计算中经常出现。此外,还有一个不容忽视的大数危机(大数处理中精度损失)问题。看到大家在群里不止一次讨论过这个问题,周末在另一个“Nodejs技术栈-交流群”里又讨论了这个问题。当时群里大家一起讨论。挺好的,周末大家在群里讨论的就到这里了。这个问题我之前也分享过,现在整理出来分享给大家。前端也是适用的,因为大家都用同一种语言JavaScript。JavaScript中最大的安全整数在开始本节之前,希望大家可以提前了解一些JavaScript浮点数方面的知识。上一篇JavaScript浮点数之谜:为什么0.1+0.2不等于0.3?很好的介绍了浮点数的存储原理以及为什么会丢失精度(建议提前阅读)。IEEE754双精度浮点数(Double64Bits)的尾数部分用于存储整数的有效位数,为52位,省略1位可保存的实际值是。Math.pow(2,53)//9007199254740992Number.MAX_SAFE_INTEGER//最大安全整数9007199254740991Number.MIN_SAFE_INTEGER//最小安全整数-9007199254740991只要不超过JavaScript中的最大安全整数和最小安全整数范围,这是安全的。大数处理精度损失问题复现示例1在Chrome控制台或Node.js运行环境中执行以下代码时,会出现如下结果,What?为什么我定义的200000436035958034会转义为200000436035958050,了解了JavaScript浮点数的存储原理后,你应该明白,此时已经触发了JavaScript的最大安全整数范围。constnum=200000436035958034;console.log(num);//200000436035958050例子2下面的例子通过stream读取传递过来的数据,保存在字符串数据中,因为传递的是一个application/json协议数据,我们需要将数据反序列化成obj进行业务处理。consthttp=require('http');http.createServer((req,res)=>{if(req.method==='POST'){letdata='';req.on('data',chunk=>{data+=chunk;});req.on('end',()=>{console.log('未被JSON反序列化:',data);try{//反序列化为obj对象,使用constobj来处理业务=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("InvalidJSON");}});}else{res.end('OK');}}).listen(3000)被调用运行上述程序后在POSTMAN中,200000436035958034是一个很大的值。下面是输出结果。发现不用JSON序列化就一切正常。当程序执行JSON.parse()时,再次出现精度问题。为什么是这样?JSON转换和大数值精度之间有什么棘手??不带JSON反序列化:{"id":200000436035958034}JSON反序列化后:{id:200000436035958050}JSON反序列化后:{id:200000436035958050}这个问题其实也遇到过,并且它的发生方式是调用第三方接口获取一个值很大的参数。结果在JSON之后出现了类似的问题。下面我们来分析一下。用于解析大数字的JSON序列化有何棘手之处?首先了解JSON数据格式标准,InternetEngineeringTaskForce7159,简称(IETF7159),是一种轻量级的、基于文本的、与语言无关的数据交互格式,源于ECMAScript编程语言标准。https://www.rfc-editor.org/rfc/rfc7159.txt访问此地址可查看协议相关内容。本节我们需要关注的是“JSON的价值是什么?”上面的协议规定必须是object、array、number、string这四种数据类型,也可以是false、null、true这三个值。至此,谜底已经揭晓。解析JSON时,默认会转换其他类型的编码。对应我们例子中的大值,默认会编码为数字类型,这才是造成精度损失的真正原因。大数计算的解决方案1.字符串转换的常用方法这是前后端交互中常用的解决方案。例如,订单号在Java中使用数值类型long类型进行存储。最大值是2.Square的64倍,而在JS中是Number.MAX_SAFE_INTEGER(Math.pow(2,53)-1)。显然,如果超过了JS能表达的最大安全值,精度就会损失。最好的解决办法是将订单号从值类型转换成字符串返回给前端处理。这是工作对接过程中遇到的一个真实的坑。2.新希望BigIntBigint是JavaScript中的一种新数据类型,可以用来对超出Number最大安全范围的整数进行运算。创建BigInt方法一种方法是在数字后面加上n200000436035958034n;//200000436035958034n创建BigInt方法二另一种方法是使用构造函数BigInt(),使用时也需要注意BigInt在使用字符串的时候最好使用字符串,否则还是会出现精度问题。官方文档也提到了这个github.com/tc39/proposal-bigint#gotchas--exceptionscalledintractablediseasesBigInt('200000436035958034')//200000436035958034n//注意使用字符串否则它会被转义BigInt(200000436035958034)//200000436035958048n这不是一个正确的结果检测类型BigInt是一种新的数据类型,所以它不完全等于Number,例如1n不会全部相等to1.typeof200000436035958034n//bigint1n===1//falseoperationBitInt支持常用的运算符,但不要和Number混用,请始终保持一致。//Correct200000436035958034n+1n//200000436035958035n//Error200000436035958034n+1^TypeError:CannotmixBigIntandothertypes,useexplicitconversionsBigIntconvertedtoStringString(200000435002034n)/4035958034//或者下面的方法(200000436035958034n).toString()//200000436035958034和JSON的冲突通过JSON.parse('{"id":200000436035958034}解析')会造成精度损失。现在出现了一个BigInt,用下面的方法能不能正常解析呢?JSON.parse('{"id":200000436035958034n}');运行上面程序后,会出现ASyntaxError:UnexpectedtokenninJSONatposition25错误,最麻烦的是这里,因为JSON是一种比较广泛的数据协议类型,影响范围非常广,不能轻易变了。这个问题也在TC39proposal-bigint存储库github.comtc39/proposal-bigint/issues/24中提出截至目前,该提案尚未添加到JSON,因为这会破坏JSON的格式,可能导致无法解析。BigInt支持BigInt的提案已经进入Stage4,已经在Chrome、Node、Firefox和Babel中发布,在Node.js中支持的版本是12+。BigInt总结我们用BigInt做一些计算是没有问题的,但是在和第三方接口交互的时候,如果JSON字符串序列化遇到一些大数的问题,还是会丢失精度。显然这是由于与JSON冲突,下面给出第三种选择。3.第三方库也可以通过一些第三方库来解决,但是你可能会奇怪为什么这么复杂?转成字符串不是大家都很乐意吗,但是有时候需要连接第三方接口,获取的数据包含这么大的数字,遇到那种拒绝改的,业务必须完成!下面是第三个实施方案。也拿我们上面第二个大数处理丢失精度问题复现的例子来说明,使用json-bigint库来解决。知道JSON规范和JavaScript的冲突后,不要直接使用JSON.parse()。接收到数据流后,先通过string方法解析。使用json-bigint库,它会自动将2的53次方的值转换成BigInt类型,然后设置一个参数storeAsString:true会自动将BigInt转换成字符串。consthttp=require('http');constJSONbig=require('json-bigint')({'storeAsString':true});http.createServer((req,res)=>{if(req.method==='POST'){letdata='';req.on('data',chunk=>{data+=chunk;});req.on('end',()=>{try{//使用第三方库JSON序列化constobj=JSONbig.parse(data)console.log('AfterJSONdeserialization:',obj);res.setHeader("Content-Type","application/json");res.end(data);}catch(e){console.error(e);res.statusCode=400;res.end("InvalidJSON");}});}else{res.end('OK');}})。listen(3000)再次验证,会看到如下结果,这次是正确的,完美解决了问题!JSON反序列化后id值:{id:'200000436035958034'}json-bigint结合Request客户端介绍axios、node-fetch、undici、undici-fetch请求客户端如何结合json-bigint处理大数.模拟服务器使用BigInt创建一个大数来模拟服务器返回的数据。这时,如果被请求的客户端不处理,就会丢失准确性。consthttp=require('http');constJSONbig=require('json-bigint')({'storeAsString':true});http.createServer((req,res)=>{res.end(JSONbig.stringify({num:BigInt('200000436035958034')}))}).listen(3000)axios创建了一个axios请求实例request,其中的transformResponse属性我们对原始响应数据做了一些自定义处理。constaxios=require('axios').default;constJSONbig=require('json-bigint')({'storeAsString':true});constrequest=axios.create({baseURL:'http://localhost:3000',transformResponse:[function(data){returnJSONbig.parse(data)}],});request({url:'/api/test'}).then(response=>{//200000436035958034console.log(响应.data.num);});node-fetchnode-fetch在Node.js中也被大量使用。一种方法是对返回的文本数据进行处理,其他更方便的方法没有深入研究。constfetch=require('node-fetch');constJSONbig=require('json-bigint')({'storeAsString':true});fetch('http://localhost:3000/api/data').then(asyncres=>JSONbig.parse(awaitres.text())).then(data=>console.log(data.num));undirequest这个已经被标记为obsolete的客户端就不介绍了,另外一个值得关注的是Node.js请求客户端undici,我在上一段也写了一篇文章介绍request已经过时——推荐一个超快的Node.jsHTTP客户端未指明。constundici=require('undici');constJSONbig=require('json-bigint')({'storeAsString':true});constclient=newundici.Client('http://localhost:3000');(async()=>{const{body}=awaitclient.request({path:'/api',method:'GET',});body.setEncoding('utf8');letstr='';forawait(constchunkofbody){str+=chunk;}console.log(JSONbig.parse(str));//200000436035958034console.log(JSON.parse(str));//200000436035958050精度损失})();undici-fetchundici-fetch是构建在undici之上的WHATWGfetch实现,类似于node-fetch。constfetch=require('undici-fetch');constJSONbig=require('json-bigint')({'storeAsString':true});(async()=>{constres=awaitfetch('http://localhost:3000');constjson=JSONbig.parse(awaitres.text());console.log(json.num);//200000436035958034})();总结本文提出大数据丢失精度的一些原因数,同时给出了几种解法。如果遇到类似问题,可以参考。还是建议大家在设计系统的时候遵循双精度浮点数的规范。在查找问题的过程中,你可能会看到有的使用正则表达式来匹配。个人不推荐。首先,正则化本身是一个耗时的过程。操作,第二个操作需要找一些匹配规则,一不小心,可能会将返回结果中的值全部转成字符串,这是不可行的。本文转载自微信公众号《Nodejs技术栈》,可通过以下二维码关注。转载本文请联系Nodejs技术栈公众号。
