问题描述:在使用JS过程中,经常会涉及到数值计算操作,比如在商品加入购物车时计算商品价格等。计算时经常会出现精度问题,比如:24.9 * 1.2 = 29.879999999999995
原因
Javascript 遵循 IEEE 754 规范,使用64位固定长度来表示,也就是标准的 double 双精度浮点数(所有遵循该规范的语言都是如此),该规范下浮点数会出现如下情况:
0.1 + 0.2 === 0.3 // false
简单来说,二进制浮点数中的 0.1 和 0.2 并不是十分精确,所以它们相加结果并不是刚好等于 0.3,而是一个比较接近的数字 0.30000000000000004 (具体原因请参考:https://github.com/camsong/blog/issues/9)。
解决方案
方案一:使用 toFixed() 方法
tofixed(..)
方法可把 Number
四舍五入为指定小数位数的数字,返回值是字符串
let price = (24.9 * 1.2).toFixed(2) // '29.88'
toFixed()
方法虽然能比较简单的解决问题,不过它是有 BUG 的,如:1.005.toFixed(2)
这个表达式的结果是 1.00
而不是 1.01
。
原因:1.005
实际对应的数字是 1.00499999999999989
,在四舍五入时被全部舍去。
方案二:把计算的数字先升级为整数,计算完成后再降级(除以10的 n 次幂)
常规的把数字升级为整数的方案是(乘以 10 的 n 次幂),如下:
// 升级前
0.1 + 0.2 === 0.3 // false
// 升级后
(0.1 * 10 + 0.2 * 10) / 10 === 0.3 // true
这种升级方案会有问题:
32.09 * 100 = 3209.0000000000005
所以上面那种升级方案是不可靠的。最后的解决方案是:我们可以把数字的小数点直接抹掉,记录小数的位数,等计算结果完成后直接降级,如下:
/**
* @method 基本运算操作
*
* @param { Number } a 操作数
* @param { Number } b 操作数
* @param { String } operator 操作符:加 -> 1;减 -> 2;乘 -> 3;除 -> 4;
*/
function operation(a, b, operator) {
// 先将数字转为字符串
a = a.toString()
b = b.toString()
// 记录小数位数
let aLen = (a.split('.')[1] || '').length
let bLen = (b.split('.')[1] || '').length
let maxLen = Math.max(aLen, bLen)
// 将操作数转换为整数
a = Number(a.replace(/\./, ''))
b = Number(b.replace(/\./, ''))
let result
switch(operator) {
case 1:
if (aLen === bLen) {
result = a + b
} else if (aLen > bLen) {
result = a + b * Math.pow(10, (aLen - bLen))
} else {
result = a * Math.pow(10, (bLen - aLen)) + b
}
return result / Math.pow(10, maxLen)
case 2:
if (aLen === bLen) {
result = a - b
} else if (aLen > bLen) {
result = a - b * Math.pow(10, (aLen - bLen))
} else {
result = a * Math.pow(10, (bLen - aLen)) - b
}
return result / Math.pow(10, maxLen)
case 3:
result = (a * b) / (Math.pow(10, aLen) * Math.pow(10, bLen))
return result
case 4:
result = (a / b) * (Math.pow(10, bLen) / Math.pow(10, aLen))
return result
}
}
参考资料
- JavaScript 浮点数陷阱及解法
- JS中浮点数精度问题
- 《你不知道的JavaScript(中卷)》