0.1 + 0.2 为什么不等于 0.3?怎么解决?
Number 是 IEEE 754 双精度二进制浮点数,0.1 和 0.2 在二进制里是无限循环小数,只能存最接近的近似值,相加后误差显露出来。比较时用误差范围,金额换成整数单位计算,显示时再格式化,需要精确十进制时用十进制库。
简答
在控制台输入
0.1 + 0.2,得到的是0.30000000000000004,所以它和0.3不相等。原因是 JavaScript 的
Number是 IEEE 754 双精度二进制浮点数。十进制的 0.1 换成二进制是无限循环小数,只能存一个最接近的近似值,0.2、0.3 也一样。两个近似值相加,结果恰好不是 0.3 的那个近似值。这不是 JavaScript 独有的问题,用同样格式的语言都会这样。解决办法看场景。判断相等时比较差值是否在一个很小的范围内,比如
Number.EPSILON;金额这类要求精确的,换成“分”这样的整数来算;只是显示时,用toFixed或Intl.NumberFormat格式化。要注意
toFixed只是按存储的真实值格式化,结果是字符串,也不能保证按十进制四舍五入。要做大量精确的十进制运算,用专门的十进制库更稳妥。
存进去的 0.1 到底是多少
toPrecision 可以让数字多显示几位,看到它真实存储的值。
console.log(0.1 + 0.2); // 0.30000000000000004
console.log((0.1).toPrecision(21)); // 0.100000000000000005551
console.log(Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON); // true
console.log((10 + 20) / 100 === 0.3); // true
console.log(Number((0.1 + 0.2).toFixed(10)) === 0.3); // true
平时打印 0.1 显示的是 0.1,为什么第二行多出一串尾巴?因为数字转字符串时,引擎会输出“能唯一还原这个值的最短十进制”,0.1 的近似值最短就写成 0.1。可是多要几位,就能看到它实际比 0.1 大一点点。
0.1 和 0.2 都偏大一点,加起来的误差比 0.3 自己的近似值误差更大,最终落在了另一个可表示的数上,最短写法就是 0.30000000000000004。
比较时怎样容忍误差
第三行用差值和 Number.EPSILON 比较。MDN 对它的定义是 1 和大于 1 的最小浮点数之间的差。它适合比较 1 附近的数,数值很大时误差也会变大,要按比例放大容差,详见《浮点精度、安全整数和 BigInt 有哪些边界?》。
计算金额怎样避免误差
第四行先把元换成分,用整数 10 和 20 相加,最后才除以 100。整数在安全范围内可以精确表示和相加,所以不会有误差。实际项目里,价格从接口拿到时就用“分”这样的整数,所有加减乘都在整数上做,只在展示时转成元。
第五行是另一种常见的补救:先用 toFixed(10) 截到 10 位小数再转回数字,把末尾的误差抹掉。它能应付展示和简单比较,但 toFixed 返回的是字符串,舍入也是按存储值做的,比如 (1.005).toFixed(2) 得到 '1.00',所以不能当作精确计算方案。
常见追问
为什么 0.1 + 0.3 等于 0.4,没有出问题?
因为它们的近似值误差相加后,正好落在 0.4 的那个近似值上,打印出来就是
0.4。是否“看起来正确”取决于具体数字,不能因为某次结果对了就认为浮点运算是精确的。
BigInt 能解决小数精度问题吗?
不能。BigInt 只表示整数。可以把金额放大成整数后用 BigInt 计算,处理超过安全整数范围的大金额,但小数部分的换算规则要自己定好。