相信各位前端小伙伴在日常工作中不免会涉及到使用 JavaScript 处理 数值 相关的操作,例如 数值计算、保留指定小数位、接口返回数值过大 等等,这些操作都有可能导致原本正常的数值在 JavaScript 中确表现得异常(即 精度丢失),这也是被很多开发者诟病的一点(你该不会还没踩过坑吧!),当然包括很多 后端开发者(不止一次的被问到这个问题)。
本文主要包含 精度丢失场景、精度丢失原因、解决方案 等方面的内容,文中若有不正确的地方欢迎在评论区分享你的见解。
数值计算在前端的应用还不算少,但涉及 浮点数 参与计算时可能会出现精度丢失,如下:
所谓 超过最值(最大、最小值) 指的是超过了 Number.MIN_SAFE_INTEGER(- 9007199254740991),即 +(2^53 – 1) 或 Number.MAX_SAFE_INTEGER(+ 9007199254740991),即 -(2^53 – 1) 范围的值,项目中最常见的就是如下几种情况:
除了上述对涉及浮点数计算、超过最值的场景之外,我们通常还会对数值进行保留指定小数位的处理,而部分开发者可能会直接使用 Number.prototype.toFixed 来实现,但这个方法却并不能保证我们期望的效果,例如保留小数位时需要进行 四舍五入 时就会有问题,如下:
js
复制代码
console.log(1.595.toFixed(2)) // 1.59 ——> 期望为:1.60 console.log(1.585.toFixed(2)) // 1.58 ——> 期望为:1.59 console.log(1.575.toFixed(2)) // 1.57 ——> 期望为:1.58 console.log(1.565.toFixed(2)) // 1.56 ——> 期望为:1.57 console.log(1.555.toFixed(2)) // 1.55 ——> 期望为:1.56 console.log(1.545.toFixed(2)) // 1.54 ——> 期望为:1.55 console.log(1.535.toFixed(2)) // 1.53 ——> 期望为:1.54 console.log(1.525.toFixed(2)) // 1.52 ——> 期望为:1.53 console.log(1.515.toFixed(2)) // 1.51 ——> 期望为:1.52 console.log(1.505.toFixed(2)) // 1.50 ——> 期望为:1.51
计算机内部实际上只能 存储/识别 二进制,因此 文档、图片、数字 等都会被转换为 二进制,而对于数字而言,虽然我们看到的是 十进制 的表示结果,但实际上会底层会进行 十进制 和 二进制 的相互转换,而这个转换过程就有可能会出现 精度丢失,因为十进制转二进制后可能产生 无限循环 部分,而 实际存储空间是有限的。
Javascript 中的数字存储使用了 IEEE 754 中规定的 双精度浮点数 数据类型,双精度浮点数使用 64 位(8 字节) 来存储一个 浮点数,可以表示二进位制的 53 位 有效数字,即 (0-52 位为 1) 111...111 = (53 位为 1,0-52 位为 0) 1000...000 - 1,也就是 2^53 - 1,而这也就是 JavaScript 中 Number.MAX_SAFE_INTEGER(+ 9007199254740991) 对应的值。
双精度浮点数(double) 由如下几部分组成:
有了上面的公式,接下来我们来演示一下一个十进制浮点数是如何以 双精度浮点数 的形式被存储到计算机中的,其大致分为如下两步:
下面我们通过 263.3 这个数值来演示。
分别将 263.3 的 整数部分 263 和 小数部分 0.3 转为对应的 二进制数,这里你可以使用便捷的 在线转换工具,也可选择手动计算:
关于这个方法的舍入方式,目前最多的说法就是 银行家算法 ,的确在大多情况下确实能够符合 银行家算法 的规则,但是部分情况就并不符合其规则,因此严格意义上来讲 Number.prototype.toFixed 并不算是使用了 银行家算法,如果你要问为什么,请看 ECMAScript® 2024 Language Specification (tc39.es),在下文都会提及。
所谓银行家算法用一句话概括为:
四舍六入五考虑,五后 有数 就进一,五后 无数 看 奇偶,五前 为偶当 舍去,五后 为奇要 进一
用例子来验证一下:
// 四舍
(1.1341).toFixed(2) = '1.13'
// 六入
(1.1361).toFixed(2) = '1.14'
// 五后 有数 ,进一
(1.1351).toFixed(2) = '1.14'
// 五后 无数,看奇偶,五前为 3 奇数,进一
(1.1350).toFixed(2) = '1.14'
// 五后 无数,看奇偶,五前为 0 偶数,舍去
(1.1050).toFixed(2) = '1.10'
看起来没有问题是吧!
// 五后 有数,应进一
(1.1051).toFixed(2) = 1.11 (正确 √)
(1.105).toPrecision(17) = '1.1050000000000000' // 精度
// 五后 无数,看奇偶,五前为 0 偶数,应舍去
(1.105).toFixed(2) = 1.10 (正确 √)
// 五后 无数,看奇偶,五前为 2 偶数,应舍去
(1.125).toFixed(2) = 1.13 (不正确 ×)
1.125.toPrecision(17) = '1.1250000000000000' // 精度
// 五后 无数,看奇偶,五前为 4 偶数,应舍去
(1.145).toFixed(2) = 1.15 (不正确 ×)
1.145.toPrecision(17) = '1.1450000000000000' // 精度
// 五后 无数,看奇偶,五前为 6 偶数,应舍去
(1.165).toFixed(2) = 1.17 (不正确 ×)
1.165.toPrecision(17) = '1.1650000000000000' // 精度
// 五后 无数,看奇偶,五前为 8 偶数,应舍去
(1.185).toFixed(2) = 1.19 (不正确 ×)
1.185.toPrecision(17) = '1.1850000000000001' // 精度
一眼望上去是不是觉得看不懂,那么这里就来尝试解释一下这个标准的内容吧(掺杂个人理解)!
不多说了,还是用 (1.125).toFixed(2) = 1.13 举个栗子吧!
还不会,再来个 (-1.105).toFixed(2) = -1.10 的栗子吧!
虽然知道了 精度丢失 的原因,也知道了 toFixed 舍入 的逻辑,但是实际上在进行计算时,我们还是希望按照实际看到的数值来进行计算或舍入,而不是底层转换过的值。
需要的自行查阅:
浮点数在 JavaScript 中经底层转换后可能会有精度丢失,但是 安全范围内的整数 却不会丢失,那么我们就可以先将 浮点数 转成 整数 进行计算后,再将计算结果成为浮点数。
以 0.1 + 0.2 = 0.30000000000000004 举个例子,如下:
【注意】这并 非最优解,因为不是所有的浮点数都刚好被转成整数,例如 关键就在 (44.8976).toPrecision(53) 取精度后的结果小数点后四位和实际值有误差,导致最后乘积的结果也是非正确值。
前面提到的 后端返回 或 前端计算 产生的超过 安全范围的值,我们可以使用 BigInt 来处理,这是新增的原始值类型,它提供了一种方法来表示 大于 2^53 - 1 的整数。
【注意】这里的 BigInt 可不能用来处理后端返回超过安全范围内的值(如 id),因为当我们需要再将这些值转为 BigInt 之前就已经发生了精度丢失,所以在进行转换是无意义的,此时最好的方式就是接口以 字符串 的形式返回对应的值
既然 Number.prototype.toFixed() 的舍入方法并不是我们需要的,那么我们可以直接将其重写成符合的即可,例如:
Number.prototype.toFixed=function (d) {
var s=this+"";
if(!d)d=0;
if(s.indexOf(".")==-1)s+=".";
s+=new Array(d+1).join("0");
if(new RegExp("^(-|+)?(d+(.d{0,"+(d+1)+"})?)d*#34;).test(s)){
var s="0"+RegExp.$2,pm=RegExp.$1,a=RegExp.$3.length,b=true;
if(a==d+2){
a=s.match(/d/g);
if(parseInt(a[a.length-1])>4){
for(var i=a.length-2;i>=0;i--){
a[i]=parseInt(a[i])+1;
if(a[i]==10){
a[i]=0;
b=i!=1;
}else break;
}
}
s=a.join("").replace(new RegExp("(d+)(d{"+d+"})d#34;),"$1.$2");
}if(b)s=s.substr(1);
return (pm+s).replace(/.$/,"");
}return this+"";
}
原文链接:https://juejin.cn/post/7264208575973621815
页面更新:2024-03-10
本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828
© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号