先记住这个答案
toFixed(n) 把数字格式化成保留 n 位小数的字符串,返回类型是 string,所以才能表示 '1.00' 这种末尾带 0 的结果。它的舍入规则是标准的四舍五入,但舍入的是数字在内存里的真实值,而不是你在代码里写的十进制。1.005 用二进制存不准,实际值是 1.00499999999999989…,第三位小数其实是 4,所以保留两位得到 '1.00'。
toFixed返回字符串,参与运算前要转回数字- 舍入按真实二进制值,不是按你写的十进制
1.005的二进制近似会影响舍入- 绝对值不小于 1e21 时直接返回科学计数法字符串
看上去「舍错」的几个例子
把几个数的 toFixed 结果和它们的真实值放在一起看,就知道问题出在哪。
const cases = [[1.005, 2], [1.045, 2], [1.255, 2], [1.45, 1], [2.5, 0]]
for (const [n, digits] of cases) {
// toPrecision(20) 能把内存里的真实值多显示几位
console.log(n, '->', n.toFixed(digits), '真实值', n.toPrecision(20))
}
console.log(typeof (1).toFixed(2), (1).toFixed(2))查看输出与解释
1.005 -> 1.00 真实值 1.0049999999999998934
1.045 -> 1.04 真实值 1.0449999999999999289
1.255 -> 1.25 真实值 1.2549999999999998934
1.45 -> 1.4 真实值 1.4499999999999999556
2.5 -> 3 真实值 2.5000000000000000000
string 1.00最后一行先看:返回值的类型是 string。前面几行看真实值那一列就明白了,1.005、1.045、1.255、1.45 存进内存后都比字面值小一点点,舍入位变成了 4,于是全部舍掉。2.5 可以用二进制精确表示,所以正常进位成 3。
也就是说,toFixed 本身的算法没有错,错的是「1.005」这个数在 Number 里根本不存在。你拿到的是离它最近的一个二进制浮点数,而这个数恰好略小于 1.005。
几个容易被忽视的行为
除了舍入,toFixed 还有几处跟直觉不一样的地方,写格式化函数时会碰到。
- 大数返回科学计数法
- 数值的绝对值大于等于 1e21 时,
toFixed不再按位数格式化,而是直接返回和String()一样的结果,(1e21).toFixed(2)得到'1e+21'。另外,即使小于 1e21,只要超过 2^53,整数部分的末几位本来就不精确,保留小数也没有意义。 - 负数会出现
-0.00 (-0.0001).toFixed(2)得到'-0.00'。数值很小的负数舍到 0 以后,负号仍然保留。显示金额时如果不想看到-0.00,要在格式化前后单独处理。- 位数参数超出范围会报错
- 参数可以是 0~100,传 101 会抛
RangeError。不传参数等于传 0,(3.7).toFixed()得到'4'。
容易答错的地方
- 以为
toFixed返回数字 (0.1 + 0.2).toFixed(2) + 1得到的是'0.301',因为字符串加数字是拼接。需要继续计算时要先Number()或parseFloat()转回来。- 以为用
Math.round(x * 100) / 100就能解决 - 乘法同样有精度问题。
1.005 * 100算出来是100.49999999999999,Math.round之后还是 100,结果仍然是 1。这个写法只是把误差换了个地方出现。 - 以为
toFixed用的是银行家舍入 - 银行家舍入是「四舍六入五取偶」。
(2.5).toFixed(0)是'3',(0.5).toFixed(0)是'1',都说明遇到正好一半时是向上进位,不是取偶。那些看起来像取偶的例子,都是真实值略小于字面值造成的。
面试官还会怎么问?
金额计算怎么做才靠谱?
最常见的做法是全程用整数「分」来存和算,只在显示时除以 100 再格式化。比如 19.9 元存成 1990 分,加减乘都是整数运算,不会有误差。注意元转分这一步本身也要处理:19.9 * 100 算出来是 1989.9999999999998,要写成 Math.round(19.9 * 100)。涉及除法、利率这类必然产生小数的计算,用 decimal.js、big.js 这类十进制库,它们用字符串或整数数组按十进制运算。
想要「正确」的四舍五入,有没有简单写法?
可以先把数字转成字符串,在字符串层面处理小数位,或者借助指数表示绕开乘法误差,比如 Number(Math.round(Number(x + 'e2')) + 'e-2')。这里字符串 '1.005e2' 会被解析成精确的 100.5,舍入后得到 101,再除回去得到 1.01。这类写法能处理大多数显示场景,但真正的金额计算还是建议用整数或十进制库。
toFixed 和 toPrecision 有什么区别?
toFixed(n) 固定小数点后的位数,toPrecision(n) 固定总的有效数字位数。(123.456).toFixed(1) 是 '123.5',(123.456).toPrecision(4) 也是 '123.5',但 (123.456).toPrecision(2) 会变成 '1.2e+2'。显示金额用前者,显示科学数据时常用后者。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。