慎用 Number.toFixed() 处理数字
最近在公司项目中碰到一个隐藏的 bug,调试许久才发现竟然是 toFixed
函数精度问题引起的,从而引发了我一系列的思考。我们都知道,计算机在二进制环境下,浮点数的计算精度会存在缺失问题,最经典的例子就是为什么 0.1+0.2
不等于 0.3?
遇到上述问题,我们自然而然会想到 toFixed
方法来四舍五入,可结果却差强人意!
toFixed() 的精度问题
我们来看一下 toFixed
在 chrome、火狐、IE 浏览器下的不同表现:
可以看到 toFixed
的四舍五入在 chrome、火狐上并不准确。
而 toFixed
在 chrome、火狐上也并不是网上流传甚广的用银行家舍入法来进行四舍五入的。
银行家舍入法的规则是“四舍六入五考虑,五后非零就进一,五后为零看奇偶,五前为偶应舍去,五前为奇要进一”。
例如银行家舍入法在 (2.55).toFixed(1) = 2.5、(3.55).toFixed(1) = 3.5 上就不符合了。
翻阅 ecmascript 规范 对 toFixed
的表述如下:
上面规范这段大概意思就是如果 toFixed
的入参小于 10 的 21 次方,那么就取一个整数 n,让 n*10^f - x 的精确值尽可能的趋近于 0,如果存在两个这样的 n,取较大的 n。这段话可能有点晦涩难懂,我们举个例子比如 1.335.toFixed(2)
上图例子中 1.335.toFixed(2)按照四舍六入五成双应该是 1.34,但是实际情况确实 1.33。这是因为 n=133 的时候让 n*10^f - x
更趋近于 0,所以最后得到的结果是 1.33。
解决方法
1.重写 toFixed()
我们可以通过重写 toFixed
的方法,来实现四舍五入:
Number.prototype.toFixed = function (length) {
var carry = 0 // 存放进位标志
var num, multiple // num 为原浮点数放大 multiple 倍后的数,multiple 为 10 的 length 次方
var str = this + '' // 将调用该方法的数字转为字符串
var dot = str.indexOf('.') // 找到小数点的位置
if (str.substr(dot + length + 1, 1) >= 5) carry = 1 // 找到要进行舍入的数的位置,手动判断是否大于等于 5,满足条件进位标志置为 1
multiple = Math.pow(10, length) // 设置浮点数要扩大的倍数
num = Math.floor(this * multiple) + carry // 去掉舍入位后的所有数,然后加上我们的手动进位数
var result = num / multiple + '' // 将进位后的整数再缩小为原浮点数
/*
* 处理进位后无小数
*/
dot = result.indexOf('.')
if (dot < 0) {
result += '.'
dot = result.indexOf('.')
}
/*
* 处理多次进位
*/
var len = result.length - (dot + 1)
if (len < length) {
for (var i = 0; i < length - len; i++) {
result += 0
}
}
return result
}
该方法的大致思路是首先找到舍入位,判断该位置是否大于等于 5,条件成立手动进一位,然后通过参数大小将原浮点数放大 10 的参数指数倍,然后再将包括舍入位后的位数利用 Math.floor
全部去掉,根据我们之前的手动进位来确定是否进位。
2.high-precision-four-fundamental-rules
在 GitHub 上找到一个 高精度的基本四则运算 npm 包 ,用来弥补原生 JS 中 toFixed
方法计算精度缺失的不足,该作者用四舍五入算法重写了改方法,并封装成 npm 包!
// 安装
$ npm install high-precision-four-fundamental-rules --save
// 使用
import {add, subtract, multiply, divide} from 'high-precision-four-fundamental-rules';
add(1, 2, 4); // '3.0000'
subtract(1, 2, 3); // '-1.000';
multiply(1, 2, 2); // '2.00';
divide(1, 3, 7); // '0.3333333';
参考文章
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论