JavaScript 中的数字以 64 位浮点数的格式表示,遵从IEEE 754-1985这一用二进制表示数字的工业标准。其中的 52 位用于分数部分,即存储精度;11 位用于指数部分,即表示小数点的位置;1 位用于符号位,表示正负。如下图所示:
对于 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 | console.log((0.1).toFixed(16)) |
这进一步说明超过 16 位的十进制数,使用 64 位浮点数的二进制表示会产生精度丢失。与此同时,我们注意到:十进制数 0.1
是无法用二进制来精确表示的,这正如 1 / 3
无法在十进制下用有限位数来精确表示,是一样的道理。这里引出了一个问题,什么样的数字在十进制下无法精确表示?什么样的数字在二进制下无法精确表示?
个人对进制的思考
进制即进位计数制,十进制即逢十进一,二进制即逢二进一,N 进制即逢 N 进一;从相反的一面来看,N 也是基数,即在 N 进制下如果要产生小数,便是用 N 为基数去分割。为了方便说明,这里以十进制为例。在十进制下,要产生可以精确表示的小数就是用基数 10 去分割,所以 1 / 10
是可以精确表示的,又由于 10
本身并非素数,可以由 2 x 5
得来,所以十进制下,只有分母表述成如下方式的小数才能精确表示:
1 | 2 ** x * 5 ** y |
考虑常见的 1 / 2
到 1 / 10
:
1 | 1 / 2 -> 2**1 * 5**0 |
引申到二进制的环境下,只有分母表述成如下方式的二进制小数才能精确表示:
1 | 2 ** x |
所以对于十进制数 0.1
, 0.2
, 0.3
都无法用二进制来精确表示,当两个数字求和时,它们丢失的精度会加起来,这便是 0.1 + 0.2 !== 0.3
的原因。