为什么 0.1 + 0.2 !== 0.3

close-up-colorful-count

JavaScript 中的数字以 64 位浮点数的格式表示,遵从IEEE 754-1985这一用二进制表示数字的工业标准。其中的 52 位用于分数部分,即存储精度;11 位用于指数部分,即表示小数点的位置;1 位用于符号位,表示正负。如下图所示:

Double Floating Point Format

对于 64 位双精度数字而言,维基百科上给出了如下信息:

  • Width: 64 bits
  • Range at full precision: ±2.23×10e308 to ±1.80×10e308
  • Precision: Approximately 16 decimal digits

即在保证精度不丢失的前提下,64 位浮点数最多能表示 16 位的十进制数,超出这一限制则导致精度丢失。回到标题抛出的问题:0.1 + 0.2 !== 0.3,我们可以试着运行以下代码:

1
2
3
4
5
6
console.log((0.1).toFixed(16))
// 0.1000000000000000
console.log((0.1).toFixed(17))
// 0.10000000000000001
console.log((0.1).toFixed(20))
// 0.10000000000000000555

这进一步说明超过 16 位的十进制数,使用 64 位浮点数的二进制表示会产生精度丢失。与此同时,我们注意到:十进制数 0.1 是无法用二进制来精确表示的,这正如 1 / 3 无法在十进制下用有限位数来精确表示,是一样的道理。这里引出了一个问题,什么样的数字在十进制下无法精确表示?什么样的数字在二进制下无法精确表示?

个人对进制的思考

进制即进位计数制,十进制即逢十进一,二进制即逢二进一,N 进制即逢 N 进一;从相反的一面来看,N 也是基数,即在 N 进制下如果要产生小数,便是用 N 为基数去分割。为了方便说明,这里以十进制为例。在十进制下,要产生可以精确表示的小数就是用基数 10 去分割,所以 1 / 10 是可以精确表示的,又由于 10 本身并非素数,可以由 2 x 5 得来,所以十进制下,只有分母表述成如下方式的小数才能精确表示:

1
2
2 ** x * 5 ** y
// x 和 y 为 非负整数

考虑常见的 1 / 21 / 10:

1
2
3
4
5
6
7
8
9
1 / 2 -> 2**1 * 5**0
1 / 3 -> fail
1 / 4 -> 2**2 * 5**0
1 / 5 -> 2**0 * 5**1
1 / 6 -> fail
1 / 7 -> fail
1 / 8 -> 2**3 * 5**0
1 / 9 -> fail
1 / 10 -> 2**1 * 5**1

引申到二进制的环境下,只有分母表述成如下方式的二进制小数才能精确表示:

1
2
2 ** x
// x 为非负整数

所以对于十进制数 0.1, 0.2, 0.3都无法用二进制来精确表示,当两个数字求和时,它们丢失的精度会加起来,这便是 0.1 + 0.2 !== 0.3 的原因。