一、前言
先来一个题目来验证自己对于隐式转换的熟悉情况:
console.log({} - {})
console.log([] - [])
console.log([] + [1, 2])
console.log([] == ![])
console.log({} == {})
返回的结果: NaN、0、1,2、true、false
如果这几道题是否是毫无压力呢? 硬背是没有出路,必须要掌握它内在的运行逻辑才能背得更牢.
明确一点,隐式转化其实有一定难度的,涉及到的知识点并不少.有 原型链的知识点、有call的知识点、自然也涉及到了 This指向问题、构造函数等问题. 简单一点的 运算符优先级也会在学习隐式转化的时候涉及. 隐式转化是一个综合性的问题.
所以说,真正的明白了隐式转化的程序员已经有一定的知识积累了. 你要自信😊
二、包装类
包装类是我们理解隐式转化的基础工具,是理解隐式转化的根基.
Boolean()
Boolean
只有两个值: true
和false
.
Boolean(xxx)
为false
的类型叫做falsey, 或者是虚值.
包括这些0、null、undefined、false、''、NaN
有时候把+0和-0算成两个
其他返回的都是true
. 叫做truth.
⚠️需要注意的是, 不要不把Boolean()
不当成函数, 不把new Boolean()
不当成构造函数实例化的过程:
if (Boolean(false)) {
// no execute
}
if (new Boolean(flase)) {
// execute
}
console.log(typeof Boolean(false)) // boolean
console.log(typeof new Boolean(false)) // Boolean
❗️不能说包装了一层包装类的说法,就能够脱离构造函数的相关处理逻辑
Number()
Number()
也是有两种类型的结果.
分为两种的处理逻辑. 对于 基本类型的处理, 和对于 引用类型的处理.
基本类型
console.log(Number(null)) // 0
console.log(Number(undefined)) // MaM
console.log(Number('1')) // 1
console.log(Number('')) // 0
console.log(Number(NaN)) // NaN
console.log(Number(false)) // 0
console.log(Number(true)) // 1
字符串转数字,注意Number('1ad')
为NaN
, 一旦字符串中存在非数字的就转化NaN
. parseInt()是对 Number()很好的补充.
null
和undefined
的差别是让人诧异的. 但是如果从作者最初设计的规则来看的话,也是合理的:
null
表示"没有对象",即该处不应该有值。
undefined
表示"缺少值",就是此处应该有一个值,但是还没有定义。
理解null和undefined的区别是需要极大的计算机素养的. 但是有大佬说得对:
虽然这两个东西的区别确实会令初学者困扰,但掌握并理解这两个值的语义实际上和理解 prototype/scope 一样是非常重要的。
引用类型
假设我们有下面这么一个对象:
const obj = {
toString() {
return 2
},
valueOf() {
return 1
}
}
当我们使用Number()
去包围它的时候, 返回的结果是1
.
此时我修改valueOf() { return {} }
的时候.再使用Number()
去包围它的话,结果却是2了.
如果我们让valueOf()
返回的是基本类型的话, 那么是和直接使用Number()
包裹住valueOf()
的值一样,即上面基本类型的结果.
所以我们很容易得出如下这样的一个逻辑过程:
- 如果
valueOf
返回原始值,就Number包装之后返回 - 如果valueOf返回的对象,就去
toString()
方法中找 - 如果
toString()
返回原始值,就Number包装之后返回 - 如果
toString()
返回的是对象,且是自己重写的.那么就直接报错 - 如果不是重写的,那么就调用Obejct.prototye.toString方法
这个也是我们让a ==1 && a == 2 && a == 2成立的问题的解决方案之一.
String()
Object.prototype.toString
对于String()
的使用依旧使用Number()
使用的例子
const obj = {
toString() {
return 2
},
valueOf() {
return 1
}
}
当我触发String(obj)
的时候,就和Number()
完全相反.
console.log(String(obj)) // 2
直接访问的是toString()
方法.
const obj = {
toString() {
return {}
},
valueOf() {
return 1
}
}
console.log(String(obj)) // 1
但是如果toString()
返回的是引用类型的话, 就往valueOf()
方法上面找.
可以说和Number()
的完全相反,但是也符合情理 .
通过重写toString()
和valueOf()
的方法来了解内部的运行规则是一种很好的方式.
如果不重写的话,Object.prototype.toString.call(对象)
, 返回值参看Number()
部分的内容.
console.log(String({})) // [object Object]
3 < 2 < 1 和 2 < 1 < 1
两个输出都是 true
. 第一个还可以说看成是数学的方法.第二个就涉及到了隐式转换了.
- 比较 2 < 1的结果, 结果为
false
. 此时等式为false < 1
- 比较运算符, 将
false
给Number
包装类包起来. 得出的结果是0
. 0 < 1
自然是true
.
Array.prototype.toString
这个记忆上没啥好说的, 直接把外面的[]给拆了就行.
console.log(String([1])) // '1'
console.log(String([1, 2])) // '1, 2'
console.log(Array.prototype.toString.call([1])) // '1'
console.log(Array.prototype.toString.call([1, 2])) // '1, 2'
三、隐式转化触发规则
boolean的隐式转化触发
if、switch、while、for(;;)、&&、||、!、!!、? : 三元
number的隐式转化触发
只要有小学的知识都知道运算符,它是用于数字之间的计算的.在JavaScript中也是基本是一样的.
+ - * % == ~~ & | ~ ^ << <<<
等, 位运算符 、算术运算符
string 的隐式转化触发
+
且两边大于等于1个string
类型. 除了有 symbol类型之外.
console.log(1 + '2' + '2') // '122'
console.log(1 + + '2' + '2') // '32'
console.log('A' - 'B' + '2') // 'NaN2'
console.log('A' - 'B' + 2) // NaN
分析这串代码:
- 第一个很简单, 符合string隐式转化的条件,
+
号两边都有至少一个字符串, 所以全部String()
包裹之后再处理 +'2'
是一元运算符, 先Number(2)
. 所以这里变成了1 + 2 + '2'
, 那么就是前面两个先进行运算.'A' - 'B'
很显然不符合条件.所以触发的是Number()
的隐式转化- 同上
四、需要注意的点:
console.log((123).toString()) // 123
console.log(undefined.toString()) // 报错
console.log(null.toString()) // 报错
undefined和null并没有包装类,它们是基础类型,所以没有toString()
方法
五、一道让我掉过坑的面试题:
console.log([] == ![]) // true
!
的运算符比==
的高.所以这个代码可以分成三部分. []
和==
,以及![]
.
- 看到 等号 这个比较运算法就应该明白 等号 两边都要转化成Number类型
- 从左到右的话,
Number([])
,[]
是引用类型,无法直接拿到原始值 valueOf
拿不到值,就走Array.prototype.toString.call([])
.从上面可以知道, 它返回的是去掉[],即字符串''
.- 此时左边为
Number('')
. 所以左边返回的自然是0
.
此时转变成0 == ![]
. 接下来转化右边的:
- 在
Boolean()
一节当中,就可以知道,除了falsey
之外,其他都是ture.而此时在!的加持下,[]
会进行Boolean()
- 此时右边为
true
.!true
就为false Number(false)
的结果为0
由此得出 0 == 0
, 返回结果为true
.
console.log({} == {}) // false
console.log({} != {}) // true
- 两边都
Number()
包裹住. toString()
之后都是[obejct Object]
Number('[obejct Object]')
为NaN
- 所以最后转化为
console.log(NaN == NaN)
的比较
NaN和任何一个值比较都不想等,包括它自己
六、另外一道让我不解的题目
66.toString() // 报错: Invalid or unexpected token
66..toString() // '66'
66.6.toString() // '66.6'
66...toString() // 报错: Unexpected token '.'
个人理解:
状态机当中,碰到数字加上 .
会默认后面跟着的也是数字.默认是0
. 即 6.
转化为6.0
.所以当.
之后加入状态机的是toString()
之一,JS引擎自然不认识这样玩意.
但是连续两个.
之后再跟着toStirng()
的话,状态机就认为浮点数已经完成了.所以此时的第二个.
就进行了隐式转换
.
由此推断三个.
过的过程是. 浮点数 加 .
后面跟着又跟着一个.
的话, 没有这样的方法,自然就报错了.