概要
JavaScript可能是世界上最流行的客户端语言,但它远非完美,也不是没有怪癖。Juan Diego Rodriguez研究了几个“荒谬的”JavaScript怪癖,并解释了它们是如何进入语言的,以及如何在自己的代码中避免它们。
特性
为什么JavaScript有这么多怪癖!?
例如:
- 为什么0.2 + 0.1等于0.30000000000000004?
- 为什么"" == false "的值为真?
JavaScript中有很多令人难以置信的决定,看起来毫无意义;有些是误解,而另一些则是设计上的直接失误。无论如何,了解这些奇怪的东西是什么以及它们为什么出现在语言中是值得的。我将分享我所认为的关于JavaScript的一些最奇怪的事情,并解释它们的意义。
0.1 + 0.2和浮点格式
我们中的许多人都嘲笑JavaScript,在主机上编写0.1 + 0.2,却发现它无法得到0.3,而是一个有趣的0.30000000000000004值。
0.1 + 0.2 = 0.30000000000000004
许多开发人员可能不知道的是,这种奇怪的结果并不是JavaScript的错!JavaScript只是遵循IEEE浮点算术标准,几乎所有其他计算机和编程语言都使用该标准来表示数字。
浮点算术到底是什么呢?
计算机必须表示各种大小的数字,从行星之间的距离到原子之间的距离。在纸上,很容易写下一个大的数字或一个小的数量,而不用担心它会占用多少空间。计算机没有这种奢侈,因为它们必须将各种数字保存为二进制和内存中的一小块空间。
以8位整数为例。在二进制中,它可以保存0到255之间的整数。
这里的关键字是整数。它不能表示它们之间的任何小数。为了解决这个问题,我们可以在8位的某个地方添加一个虚数小数点,这样小数点前的位就用来表示整数部分,其余的用来表示小数部分。因为这个点总是在同一个虚位上,所以它被称为定点小数。但是它带来了很大的代价,因为范围从0到255正好减少到0到15.9375。
更高的精度意味着牺牲射程,反之亦然。我们还必须考虑到计算机需要满足大量不同需求的用户。如果测量值有一点点偏差,比如百分之一厘米,建桥的工程师不会太担心。但是,另一方面,同样的百分之一厘米最终可能会花费更多的钱来制造一个微芯片。所需的精度是不同的,错误的后果也会有所不同。
另一个需要考虑的问题是数字存储在内存中的大小,因为将长数字存储在兆字节这样的大小是不可行的。
浮点格式的产生是由于需要精确而高效地表示大小数量。它分为三个部分:
- 表示数字是正还是负的单个位(0表示正,1表示负)。
- 包含数字的数字的有效尾数。
- 指数指定相对于尾数开头的十进制(或二进制)点的位置,类似于科学记数法的工作原理。因此,点可以移动到任何位置,因此是浮点数。
8位浮点格式可以表示0.0078到480(及其负数)之间的数字,但请注意,浮点表示不能表示该范围内的所有数字。这是不可能的,因为8位只能表示256个不同的值。不可避免地,许多数字不能准确地表示。山脉上有一些缺口。当然,计算机使用更多的位来提高精度和范围,通常使用32位和64位,但不可能准确地表示所有的数字,如果考虑到我们获得的范围和我们节省的内存,这是一个很小的代价。
确切的动态要复杂得多,但现在,我们只需要理解,虽然这种格式允许我们在很大范围内表达数字,但当它们变得太大时,它会失去精度(可表示值之间的差距变得更大)。例如,JavaScript数字以双精度浮点格式表示,即每个数字在内存中以64位表示,剩下53位表示尾数。这意味着JavaScript只能安全地表示-(253 - 1)和253 - 1之间的整数,而不会失去精度。超过这个数,算术就没有意义了。这就是为什么我们有这个数字。MAX_SAFE_INTEGER静态数据属性,表示JavaScript中的最大安全整数,即(253 - 1)或9007199254740991。
但是0.3明显低于MAX_SAFE_INTEGER阈值,那么为什么我们不能在添加0.1和0.2时得到它呢?浮点格式难以处理一些小数。这不是浮点格式的问题,但肯定是任何数字系统的问题。
为了理解这个,我们把1 / 3表示成以10为底。
0.3
0.33
0.3333333 [...]
不管我们试着写多少个数字,结果永远不会正好是三分之一。同样地,我们也不能精确地用2进制或二进制表示一些小数。以0.2为例。我们可以把它写成以10为基底的形式,但如果我们把它写成二进制的话,最后会得到一个循环的1001,它是无限重复的。
0.001 1001 1001 1001 1001 1001 10 [...]
显然,我们不能有一个无限大的数字,所以在某些时候,尾数必须被截断,使得在这个过程中不失去精度是不可能的。如果我们尝试将0.2从双精度浮点数转换回10进制,我们将看到内存中保存的实际值:
0.200000000000000011102230246251565404236316680908203125
不是0.2!我们不能表示大量的小数值——不仅在JavaScript中,而且在几乎所有的计算机中。那么为什么运行0.2 + 0.2可以正确地计算出0.4呢?在这种情况下,不精度非常小,Javascript会将其舍入(在第16位小数处),但有时不精度足以逃避舍入机制,就像0.2 + 0.1的情况一样。如果我们尝试将0.1和0.2的实际值相加,就可以看到下面发生了什么。
这是写入0.1时保存的实际值:
0.1000000000000000055511151231257827021181583404541015625
如果我们手动将0.1和0.2的实际值加起来,我们将看到罪魁祸首:
0.3000000000000000444089209850062616169452667236328125
该值四舍五入为0.30000000000000004。您可以检查保存在float.exposed中的实际值。
总结
浮点数有其众所周知的缺陷,但它的优点大于缺点,它是世界各地的标准。从这个意义上说,当所有现代系统在不同架构中都给我们相同的0.30000000000000004结果时,这实际上是一种解脱。这可能不是你期望的结果,但这是一个你可以预测的结果。