blog
blog copied to clipboard
彻底终结 Javascript 背后的隐式类型转换
网上已经有很多 JS 隐式类型转换相关的博客, 很多面试者专门复习过此问题, 但依然挡不住面试官一个又一个的无聊小题目~
[] == false // true
!![] == true // true
!!'' == false // true
[1] == '1' // true
'' == 0 // true
'' == false // true
[1] == true // true
null == 0 // false
null == '' // false
如果你还在为这些面试题烦恼, 那恭喜你来对了地方!
看完此文你可以完全不需要背诵复杂冗长的 ECMA 规范, 用逻辑即可推理
现在, 我们假装从 JS 设计者的角度来聊聊隐式类型转换, 首先要记住, JS 作者的"初衷"是美好的, 他希望 ==
是最顺手最快捷的比较
为什么 [] == false
?
首先我们知道 []
和 false
一个是对象, 一个是布尔值, 类型不同, 需要类型转换再做比较
要注意, JS 中规定, 如果 ==
中有布尔值, 只能转换为数字, 那为什么不是转换成字符串呢?
因为如果布尔值转换成字符串那就是 'true'
和 'false'
, 那这种对比就毫无意义了
Number(true) // 1
Number(false) // 0
这也符合我们的常识, 很多语言也是类似的设定, 也就是 Bool 属于一种 Int
所以此问题可以转换成: 为什么 [] == 0
?
为什么 [] == 0
?
我们知道 Primitive(原值)和非 Primitive 比较, 需要把非 Primitive 转换成 Primitive 才可以
[]
是一个对象, 因此需要 toPrimitive()
简单的说, 大部分对象最后都是用 toString()
来转换成 Primitive
此处没聊
toPrimitive()
中的valueOf()
单纯是因为面试题很少涉及
你也许会问为啥是用 toString 不是 toNumber 之类的呢? 因为每个对象都有 toString 方法, Object.prototype.toString
, 更上层的对象也会重写 toString 方法
继续刨根问底, 为啥每个对象都有 toString 而不是 toNumber 呢?
这就是 JS 对新人友好的地方, JS 的对象都可以打印输出, 自带人性化展示, 在终端上人性化展示, 那当然是用字符串啦, 因此选择用 toString 转换 Primitive 理所因当
我们来看看数组的 toString, 数组的 toString 相当于 join()
[].toString() // ''
[1].toString() // '1'
[1, 2, 3, 4].toString() // '1,2,3,4'
所以此问题可以转换成: 为什么 '' == 0
?
为了验证我们的想法, 我们来尝试一些更奇葩的对象和字符串的 ==
比较
[1] == '1' // true
'[object Object]' == {} // true
({}).toString()
是 [object Object]
, 所以 '[object Object]' == {}
果然也是 true
为什么 '' == 0
?
字符串和数字比较会把字符串转换成数字
问题来了, 为什么不是把数字转换成字符串呢? 从设计者的角度可能会这样想
都转成数字能处理的复杂场景更多, 容错性更高!
' 1 ' == 1.0 // true
'12.10' == 12.1 // true
这样对开发者就会很方便
要注意, 字符串转成数字不是用的 parseInt()
或者 parseFloat()
, 而是 Number()
Number('') // 0
Number('abc') // NaN
所以此问题可以继续转换成: 为什么 0 == 0
? 显然就是返回 true
转换路程
推理到此结束, 我们回顾一下这个比较的转换规程
-
[] == false
-
[] == 0
-
'' == 0
-
0 == 0
看到这里我们猛然想明白了为啥 NaN 不能等于自身!
为什么 NaN !== NaN
?
我们不妨来看看 'abc' == NaN
做比较的过程
因为 NaN 也是数字类型, 所以我们需要把 'abc'
转换为数字
'abc' == NaN
相当于 Number('abc') == NaN
相当于 NaN == NaN
, 如果 NaN 可以等于自身的话, 这种情况就会返回 true
那整个隐式转换就乱套了
因此 NaN 不能等于自身也是哑巴吃黄连, 有苦说不出啊
以上纯属本人推测, 概不负责, 毕竟 Java 中的 NaN 也不能等于自身
隐式转换的恶果
NaN 不能等于自身是隐式转换最大的恶果
你可以尝试如下操作
[1, 2, NaN].indexOf(NaN) // -1
[1, 2, NaN].includes(NaN) // true
有比较的地方, 就会有 NaN 特殊处理, 否则就是不严谨
简单逻辑复杂化, 说的就是你 NaN
, 可以说 "隐式转换一时爽"~
为什么 null == 0
是 false 呢 ?
两边类型不同, 是不是也要类型转换呢?
要是能转的话确实要转, 但 null 和数字0本身已经是 Primitive 了, 没有机会再走一遍 toPrimitive()
, 因此等号两边始终无法转换成同类型, 只能返回 false
为什么 null == undefined
?
和上面的问题一样, null 和 undefined 都是 Primitive, 而且也不是字符串或者数字, 转无可转
但 JS 专门规定了 null == undefined
就是返回 true, 属于一种专门的特殊情况
The Abstract Equality Comparison Algorithm
If x is null and y is undefined, return true. If x is undefined and y is null, return true.
为什么 !![]
是 true
?
这里面不涉及任何 ==
比较, 和上面的题目完全是两类题目, 千万不可搞混
此题直接判断这个值是不是 Falsy(假值) 即可, 只要不是这几个值, 都是 true
Falsy 的值有 0
, ''
, false
, NaN
, null
, undefined
类似的问题 !![] == true
, 因为这个表达式先要计算 !![]
, 它已经是 true 了
为什么 ESLint 中会各种限制使用 ==
?
我觉得完全可以理解, ==
虽然也是一种便捷的转换, 但并不符合传统语言的习惯, 工程化企业化的项目不想用这种 "黑魔法" 也是一种正确的选择
通哥厉害👍 不过有两个小细节没写到:
- ToPrimitive其实先调用 valueOf(),如果没有原始值再调用toString(),如果依旧不是原始值会报错。
-
[] == false
其实并不等价于'' == false
,在spec里,ToPrimitive的优先度是在ToNumber之后的,所以其实是[] == false
==>[] == 0
==>'' == 0
==>0 == 0
感谢 @browsnet 指点!
1倒是知道, 不过面试用到的场景不多, 2确实是学习了
我最喜欢的是
{} > {}
{} == {}
{} >= {}
@hjin-me 大王果然变态!
@chunpu 所以真爱生命,开启 ESLint