有趣的JavaScript - 精度丢失和隐式类型转换
前端时间在社区看到有人发了一张JavaScript搞笑图,于是我找到了出处,觉得这张图很好,值得细细品味。
一张搞笑图
首先贴出这张所谓的JavaScript搞笑图,感受下JavaScript这门弱类型语言“魔性”的地方。
这张图片出自El Bruno的博客。
相信面对这张图片,很多人会跟我一样是奔溃的。但是JavaScript就是这样,既然想学好它,就要弄清楚它的一切。
下面就泡杯咖啡,跟我一起来逐一剖析图中的每一行代码与它神奇的运行结果。
可能有理解不对的地方,希望阅读本文的小伙伴能在下方留言指出。
1. typeof NaN –> “number”
在JavaScript中,所有数字的类型都为 Number
,typeof
的运算结果即为 number
。
但是JavaScript为了表达几个额外的语言场景,规定了几个例外情况:
● NaN
,即“NOT a Number”,虽然名字叫“不是数”,但NaN依旧是数字类型。
● Infinity
,无穷大。
● -Infinity
,负无穷大。
2. 9999999999999999 –> 10000000000000000
JavaScript中的Number类型有 18437736874454810627
(即2^64-2^53+3
) 个值,符合 IEEE 754-2008 规定的双精度浮点数规则。
根据双精度浮点数的定义,Number类型中有效的整数范围是-0x1fffffffffffff
⾄0x1fffffffffffff
,所以Number⽆法精确表示此范围外的整数。
说人话:JavaScript有效位数只有53位,所以JS能精确表示的最大整数就是 Math.pow(2, 53)
,十进制就是 9007199254740992
。
在JavaScript中也设置了 Number.MAX_SAFE_INTEGER
(最大安全整数)和 Number.MIN_SAFE_INTEGER
(最小安全整数)。当JavaScript在存储超过 9007199254740992
的数值时,可能会存在精度丢失的情况(超过52位的会被自动去掉)。
例如:
如果想进一步了解是怎么计算出来的,可以观察下表并结合阮一峰的这篇文章:
s | eeeeeee eeee | ffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff |
---|---|---|
1 | 11 | 52 |
3. 0.1+0.2 == 0.3 –> false
同样根据浮点数的定义,⾮整数的Number类型⽆法⽤ ==(===也不⾏)来⽐较,⼀段著名的代码:
console.log(0.1 + 0.2 == 0.3)
这⾥输出的结果是 false
,说明两边不相等的,这是浮点运算的特点,浮点数运算的精度问题导致等式左右的结果并不是严格相等,⽽是相差了个微⼩的值。
所以实际上,这⾥错误的不是结论,⽽是⽐较的⽅法,正确的⽐较⽅法是使⽤JavaScript提供的最⼩精度值:
console.log(Math.abs(0.1 + 0.2 - 0.3) <= Number.EPSILON)
检查等式左右两边差的绝对值是否⼩于最⼩精度,才是正确的⽐较浮点数的⽅法。这段代码结果就是 true
了。
4. Math.min() 和 Math.max()
查看JavaScript | MDN可知:
当 Math.min()
方法无参数时返回 Infinity
,
而 Math.max()
无参数时返回 -Infinity
。
JS能够表示的最大数值和最小数值分别为 Number.MAX_VALUE
和 Number.MIN_VALUE
,
在大多数浏览器中,它们分别是 1.7976931348623157e+308
和 5e-324
。
可以看到,它们都是正数,是绝对值中的最大和最小数值。
还有比他们更大或者更小的值,当计算结果得到一个超过数值范围的值,那么就会转成 Ifinity
(正无穷)和 -Infinity
(负无穷)。
JS也提供了2个属性保存这两个无穷值,分别是 Number.NEGATIVE_INFINITY
(负无穷)和 Number.POSITIVE_INFINITY
(正无穷)。
5. []+[] 和 []+{}
在使用一元操作符(如+
、-
、++
、--
)时,JavaScript存在隐式类型转换。
● 在应用于一个包含有效数字字符的字符串时,先将其转换为数字值。
● 在应用于一个不包含有效数字字符的字符串时,将变量的值设置为 NaN
。
● 在应用于布尔值 false
和 true
时,分别转换为 0
和 1
。
● 在应用于对象时,先调用对象的 valueOf()
方法,取的一个可供操作的值。如果结果为 NaN
,就在调用 toString()
方式转成字符串,在执行前面的操作。
未被重定义的情况下 valueOf()
方法返回指定对象的基础类型值;toString()
方法返回一个表示该对象的字符串。
在这里 []
和 {}
都是引用类型,是对象,所以先调用 valueOf
方法,后面调用 toString
方法。
第一个 []+[]
,Array对象重写了Object的valueOf方法和toString方法,valueOf返回数组本身,toString返回与没有参数(默认为逗号拼接)的 join() 方法返回的字符串相同。所以这里先返回了数组本身,无法进行“+
”操作,在调用toString方法,变成了空字符串,两个空字符串相加,所以最后输出空字符串。
[] + []
// ""
第二个 []+{}
,[]
最终转成空字符串,+
运算实际上变成了字符串拼接方法,于是 {}
调用Object的原生toString方法,转成了 "[object Object]"
,最终拼接为了 "[object Object]"
。
[] + {}
// "[object Object]"
6. {}+[]
这个和上一项有点相似,如果按照第五项中的解释去理解,是得不到结果的。为什么呢?这要说到JavaScript引擎本身解释代码的问题了。
在JavaScript解释 {}
时,有两种情况,一种是语句块,一种是对象定义。
当直接在控制台输入 {} + []
时,此时解释器将 {}
解释为语句块,即 {} + []
,所以输出就变成了 +[]
的结果,这里+
符号会强制转换,执行toNumber()
操作,空数组返回数字0
。
{} + []
// 0
如果在外面加上括号,即 ({} + [])
,那么 {}
就会被解释为对象,最后返回 "[object Object]"
。
({} + [])
// "[object Object]"
7. true+true+true === 3
有运算操作符时,Boolean类型 false
转为 0
,true
转为 1
,所以左侧结果为 3
,值相等,类型也相等,故返回 true
。
true + true + true === 3
// true
8. true - true
和上面同样的理由,转变成数值运算 1-1
,所以返回 0
。
9. true == 1
相等操作符,两侧会进行 toNumber 操作,进行值判断,不进行类型判断。所以 1 == 1
,返回 true
。
10. true === 1
全等操作符,既判断值,也判断类型,即不做类型转换,这里值虽然相同,一个为Boolean类型,一个为Number类型,所以返回 false
。
11. (! + [] + [] + ![]).length
运算符具有优先级,如下表所示:
运算符 | 描述 |
---|---|
. [] () | 字段访问、数组下标、函数调用以及表达式分组 |
++ – - ~ ! delete new typeof void | 一元运算符、返回数据类型、对象创建、未定义值 |
* / % | 乘法、除法、取模 |
+ - + | 加法、减法、字符串拼接 |
<< >> >>> | 移位 |
< <= > >= instanceof | 小于、小于等于、大于、大于等于、instanceof |
== != === !== | 等于、不等于、严格相等、非严格相等 |
& | 按位与 |
^ | 按位异或 |
丨 | 按位或 |
&& | 逻辑与 |
丨丨 | 逻辑或 |
?: | 条件 |
= oP= | 赋值、运算赋值 |
, | 多重求值 |
这里可以看到逻辑非!操作符优先级比+高,所以第一个逻辑非!先执行,相当于!(+[]),+[]进行toNumber操作返回0,然后逻辑非进行toBoolean操作返回true,然后![]执行,于是[]先进行toBoolean的操作,返回true,然后逻辑非操作变成false,然后执行从左至右+运算,即true+[]+false,变成了字符串拼接,于是返回“truefalse”,这个字符串的长度也就是9了。
12. 9 + “1”
无论是 9+"1"
,还是 "9"+1
,结果都是 "91"
。+
运算符可以是数字相加运算,也可以是字符拼接运算。但是ES规范规定了,如果+
运算符两侧存在字符串时,就调用toString()
方法,进行字符串拼接操作,所以这里结果都是 "91"
。
13. 9 - “1”
-
运算符和+
运算符不同,因为-
运算符就是数字运算减的操作,所以先转成Number类型,所以无论是 "9" -1
还是 9 -"1"
,结果都是 8
。
14. [] == 0
隐式转换,空数组在相等操作符是会转成数字0
,所以返回 true
。
写在最后
当然了,写这篇文章的目的只是兴趣使然地逐行解释下开头那张图,希望趁此机会引出几个JavaScript重(冷)要(门)知识点。
这张图里的很多知识并不是实际编程所需要的,但是如果想对JavaScript有更深的理解,还是要去好好学习的,详情可见下面的参考文档。
参考
《重学前端(winter)》
《JavaScript标准参考教程(阮一峰)》
《JavaScript高级程序设计(第3版)》