偶遇一个不准确的方法 toFixed() ,其是 JS 中用于将数字格式化为指定小数位数的方法,但有时返回的结果不够准确,展示如下:
这通常是由于 JavaScript 对浮点数的处理方式导致的。
1. 浮点数精度问题
JavaScript 中的数字是以 IEEE 754 双精度浮点数 格式存储的,遵循64位的二进制浮点数表示。这种表示方式虽然能精确表示一些数值(如整数),但对某些小数(如 0.1 或 0.2 等)无法精确表示,它们会被存储为近似值。这是因为在二进制中,有些小数无法被精确地表示,导致了舍入误差。
🌰
这里 0.1 + 0.2 并不是精确的 0.3,而是稍微大于 0.3 的近似值。这种舍入误差是由于浮点数在二进制中表示精度不足造成的。
分析:
1、存储不精确
计算机存储的是二进制格式,不能存储无限小数,需要进行截断处理,导致第一步存储不精确,截断之后为 1 进一位,截断之后为 0 保留位。
2、运算不精确
因为存储的问题,进一步导致运算出问题,但不是绝对的,可能会抵消。
3、显示不精确
浏览器会进行近似处理,而不是完全展示真实数据。
一个神奇的现象,两个不一样的数据居然返回为 true。
2. toFixed() 的行为
toFixed() 方法会将一个数字四舍五入为指定的小数位数,然后返回字符串形式的结果。
虽然它的作用看起来很简单,但由于浮点数的精度问题,有时会返回意料之外的结果。尤其是当浮点数内部表示的近似值超出人们期望时,toFixed() 会将这个误差传播到结果中。
🌰:
在 1.005 的例子中,浮点数舍入时引入了误差,toFixed(2) 结果会让人觉得舍入有问题,然而这是浮点数精度不足的体现。
十进制位数不同导致精度最后取舍有问题,究竟是1100的循环还是0011还是其他,影响不大。
3. 如何解决 toFixed 精度不准的问题
要解决浮点数精度问题,可以采用以下几种方法:
1、手动控制四舍五入的逻辑
function toFixedFix(num, decimalPlaces) {
const factor = Math.pow(10, decimalPlaces);
return (Math.round((num + Number.EPSILON) * factor) / factor).toFixed(decimalPlaces);
}
console.log(toFixedFix(1.005, 2)); // 输出 1.01
console.log(toFixedFix(10.235, 2)); // 输出 10.24
2、使用第三方库
由于 JavaScript 内置的浮点数精度问题,一些库提供了更加精确的数值处理方法,尤其适用于财务计算等对精度要求较高的场景。
🌰:BigNumber、Decimal,它可以精确处理任意大小和精度的数字。
// npm install bigdecimal.js
const BigDecimal = require('bigdecimal.js');
function toFixedBigDecimal(num, decimalPlaces) {
const bigNum = new BigDecimal(num.toString());
return bigNum.setScale(decimalPlaces, BigDecimal.ROUND_HALF_UP).toString();
}
console.log(toFixedBigDecimal(1.005, 2)); // 输出 1.01
console.log(toFixedBigDecimal(10.235, 2)); // 输出 10.24
这些库通过不同的底层实现来避免 JavaScript 浮点数的精度问题,并提供更多精确的数值操作方法。
3、使用字符串运算解决浮点误差
可以通过将数字转换为字符串,避免浮点数计算的问题。
function toFixedString(num, decimalPlaces) {
let [integerPart, decimalPart] = num.toString().split('.');
if (decimalPart && decimalPart.length > decimalPlaces) {
// 提取多余的位数进行四舍五入操作
const roundingDigit = parseInt(decimalPart[decimalPlaces], 10);
decimalPart = decimalPart.slice(0, decimalPlaces);
if (roundingDigit >= 5) {
decimalPart = (parseInt(decimalPart, 10) + 1).toString();
}
}
return parseFloat(integerPart + '.' + decimalPart).toFixed(decimalPlaces);
}
console.log(toFixedString(1.005, 2)); // 输出 1.01
console.log(toFixedString(10.235, 2)); // 输出 10.24