Skip to content

toFixed 的精度问题

toFixed 方法用于将数字转换为字符串,并保留指定的小数位数。然而,由于浮点数精度问题,结果可能并不总是精确的。

ts
(2.55).toFixed(1); // 错误, 返回 "2.5"
(2.45).toFixed(1); // 正确, 返回 "2.5"
(3.55).toFixed(1); // 错误, 返回 "3.5"
(1.55).toFixed(1); // 正确, 返回 "1.6"

存储问题

浮点数在计算机中的存储是二进制的,某些十进制数字无法准确转换成二进制表示,会变成无限循环的小数。例如,0.2.toString(2) 的结果是: 0.001100110011001100110011001100110011001100110011001101

计算机会截断它,0.2 的循环的数是 0011, 最后本来应该是0011, 却变成了 01

因此,0.2 在计算机中的存储会比实际值稍大。可以使用 toPrecision 来查看精度,toPrecision 可以指定数字的精度,从而查看计算机存储的二进制数值:

js
(0.2).toPrecision(20); // '0.20000000000000001110'

类似地,1.55 存储是偏大,3.55 存储是偏小,虽然小数的部分是一样的,但是舍去的位置却是不一样,因为计算机里面存储的精度是有限的。 1.55 存储二进制整数部分是 13.55 存储二进制整数部分是 11,导致 1.55 小数部分可以多存储一个精度。

js
(1.55).toPrecision(20); // '1.5500000000000000444'
(3.55).toPrecision(20); // '3.5499999999999998224'

不过,有些数字的存储是精确的。例如, 0.25, 0.5, 0.75。这里面有个规律,凡是可以表示为 1/2 的整数 n 次方,那么在计算机中存储都是精确的。

运算问题

尽管 0.20.3 存储时都有精度误差,但它们相加时误差相互抵消,结果是精确的。

因为 0.2 存储的二进制会多一点 0.20000000000000001110, 而 0.3 存储二进制会少一点 0.29999999999999998890, 一多一少相加就抵消了。

js
0.2 + 0.3; // 0.5

然而,0.3 减去 0.2, 由于 0.3 少一,0.2 多一点,相减就更不精确了。

js
0.3 - 0.2 = 0.09999999999999998;

显示问题

显示又是一层不精确。

即便 0.2 存储时存在精度问题,输出时通常会显示为精确的 0.2。这是因为计算机在显示时会进行近似处理:

js
var a = 0.2;
console.log(a); // 0.2

但是,对于像 0.3 - 0.2 这样的计算,精度的损失会被放大,因此显示时可能无法精确地显示为 0.1

js
0.3 - 0.2; // 0.09999999999999998

同时意味着,下面的数字显示出来是一样,因为显示是一层不精确。

js
0.199999999999999999; // 0.2
0.2000000000000000111; // 0.2

总结

为什么 1.55.toFixed(1) 返回 1.6, 而 1.45.toFixed(1) 返回 1.4?这与数字的存储精度有关。使用 toPrecision 可以查看它们的精确存储值:

js
(1.55).toPrecision(20); // '1.5500000000000000444'
(1.45).toPrecision(20); // '1.4499999999999999556'

解决方案

为避免精度问题,可以使用第三方库如 decimal.js,该库通过字符串来存储数字,并进行计算,从而避免了浮点数的精度问题。