toFixed
的精度问题
toFixed
方法用于将数字转换为字符串,并保留指定的小数位数。然而,由于浮点数精度问题,结果可能并不总是精确的。
(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 可以指定数字的精度,从而查看计算机存储的二进制数值:
(0.2).toPrecision(20); // '0.20000000000000001110'
类似地,1.55
存储是偏大,3.55
存储是偏小,虽然小数的部分是一样的,但是舍去的位置却是不一样,因为计算机里面存储的精度是有限的。 1.55
存储二进制整数部分是 1
,3.55
存储二进制整数部分是 11
,导致 1.55
小数部分可以多存储一个精度。
(1.55).toPrecision(20); // '1.5500000000000000444'
(3.55).toPrecision(20); // '3.5499999999999998224'
不过,有些数字的存储是精确的。例如, 0.25
, 0.5
, 0.75
。这里面有个规律,凡是可以表示为 1/2 的整数 n 次方,那么在计算机中存储都是精确的。
运算问题
尽管 0.2
和 0.3
存储时都有精度误差,但它们相加时误差相互抵消,结果是精确的。
因为 0.2
存储的二进制会多一点 0.20000000000000001110
, 而 0.3
存储二进制会少一点 0.29999999999999998890
, 一多一少相加就抵消了。
0.2 + 0.3; // 0.5
然而,0.3
减去 0.2
, 由于 0.3
少一,0.2
多一点,相减就更不精确了。
0.3 - 0.2 = 0.09999999999999998;
显示问题
显示又是一层不精确。
即便 0.2
存储时存在精度问题,输出时通常会显示为精确的 0.2
。这是因为计算机在显示时会进行近似处理:
var a = 0.2;
console.log(a); // 0.2
但是,对于像 0.3 - 0.2
这样的计算,精度的损失会被放大,因此显示时可能无法精确地显示为 0.1
。
0.3 - 0.2; // 0.09999999999999998
同时意味着,下面的数字显示出来是一样,因为显示是一层不精确。
0.199999999999999999; // 0.2
0.2000000000000000111; // 0.2
总结
为什么 1.55.toFixed(1)
返回 1.6
, 而 1.45.toFixed(1)
返回 1.4
?这与数字的存储精度有关。使用 toPrecision
可以查看它们的精确存储值:
(1.55).toPrecision(20); // '1.5500000000000000444'
(1.45).toPrecision(20); // '1.4499999999999999556'
解决方案
为避免精度问题,可以使用第三方库如 decimal.js
,该库通过字符串来存储数字,并进行计算,从而避免了浮点数的精度问题。