关于浮点数的更多现象以及 api 了解。
背景
我们都知道 0.1+0.2 != 0.3,原因是浮点数的存储问题。关于 javascript api 的相关现象我们也了解一下,如
2.55.toFixed(1); // 2.5
1.55.toFixed(1); // 1.6
35.41 * 100; // 3540.9999999999995
api 知识
toString(radix = 10)
- 返回该数字的字符串。
- 支持传入 radix 指定基底,范围在 2-36 之间,默认为 10。
- 如果基数为 10,并且数字的大小(忽略符号)大于或等于 10^21 或小于 10^-6,则使用科学计数法。在这种情况下,返回的字符串总是显式地指定了指数的符号。
toPrecision
- 返回一个以指定精度表示该数字的字符串。该字符串四舍五入到 precision 个有效数字。注意整数部分也计算在内,同时 0 开头认为是无效数字。0.000123.toPrecision(5),输出 '0.00012300'。
- 如果 precision 参数被省略,则与 Number.prototype.toString() 行为相同。
- 在某些情况下可能会返回指数表示法字符串,如整数位数大于指定的精度时,不使用指数表示则会明显不合理,(1234.5).toPrecision(2) 输出 '1.2e+3'。
toFixed(digit = 0)
- 返回一个表示 numObj 的字符串,但不使用指数计数法,并且小数点后有精确到 digits 位的数字。如果需要截断,则将数字四舍五入;如果小数位数不足,则小数部分用零填充,以使其具有指定长度。
- digit 指定小数点后的位数,介于 0 和 100 之间的值.
- 如果 numObj 的绝对值大于或等于 10^21,则该方法使用与 Number.prototype.toString() 相同的算法,并以指数计数法返回字符串。
- 当超过 numObj 超过最大安全整数(MAX_SAFE_INTEGER)时,toFixed 结果可能比 toString 更精确
如何解决
两个主要问题
- 问题一:toFixed 时而向上取整,时而向下取整,实际业务使用时肯定不行,那要如何解决呢
- 问题二:在不引入第三方库(bignumber.js、number-precision 和 decimal.js 等)下,我们要如何做小数计算呢
针对问题一:底层还是浮点数存储问题,我们用 toPrecision 多保留点精度看下
2.55.toPrecision(17) // '2.5499999999999998'
1.55.toPrecision(17) // '1.5500000000000000'
看到这里我们就能理解为啥会这样了,那我们要怎样才能解决这种情况呢,网上给了一种通用的解法,在四舍五入前,给数字加一个极小值,如 1e-14。
(2.55+1e-14).toFixed(1); // 2.6
针对问题二:一个更好的做法是把小数转成整数后再运算。但 35.41 * 100 的结果告诉我们,这样某些情况还是会出现问题,此时我们可以在转成整数后再 round 一次。
function add(num1, num2) {
const num1Digits = (num1.toString().split('.')[1] || '').length
const num2Digits = (num2.toString().split('.')[1] || '').length
const multiplier = 10 ** Math.max(num1Digits, num2Digits)
return (Math.round(num1 * multiplier) + Math.round(num2 * multiplier)) / multiplier
}