今天我们来讲讲JS中的类型转换机制。我们在面试中大概率碰到这道题:[] == ![]。请问它返回的是true还是false,为什么?
想要搞清楚这道题的原理,我们就得搞清楚JS中是怎么进行类型转换的。等你看完这篇文章后,你就能从容应对面试官了。
1. == VS ===
我们先来看一下双等于和三等于的区别。
对于1 === 1,我们知道,这两个一定相等,因为都是数字1。对于'1' === '1',我们也知道,这两个一定相等,因为都是字符串1。
那对于'1' === 1,它们相等吗?这类型都不一样,字符串1怎么可能等于数字1呢,肯定不相等。确实不相等,那'1' == 1,返回的是true还是false呢?
这两个是相等的。这是因为在双等于号中发生了隐式类型转换,它会把字符串1转换成数字1去比较是否相等。而三等于号不会发生隐式类型转换,如果两边的值类型都不相等,那两边一定不相等。
所以我们有个结论:
== 会发生隐式类型转换,所以只会判断值是否相等
=== 不会发生隐式类型转换,意味着就判断值和类型是否相等
2. 原始类型之间的转换
我们先来聊聊原始类型之间的转换。原始类型的转换比较简单,我们简单过一遍。我们重点介绍引用类型转原始类型。
2.1 原始类型转布尔类型
我们想把一个原始类型转布尔类型,应该怎么转。用它的构造函数转。
我们来过一遍原型类型转成布尔类型会是什么结果。
undefined转布尔:
null转布尔:
数字1转布尔:
数字0转布尔:
NaN转布尔:
所以在数字类型中,只有0和NaN转布尔为false,其它全为true。
字符串转布尔:
空字符串转布尔:
Bigint和Symbol类型因为是后来新增的,它们一定不会发生类型转换的,所以我们不用管。
原始类型转布尔我们已经看完了。
2.2 原始类型转数字
原始类型转数字用构造函数Number转。
字符串1转数字:
直接变成数字1,很好理解。
空字符串转数字:
字符串hello转数字:
undefined转数字:
null转数字:
true转数字:
false转数字:
原始类型转数字我们也过了一遍。
2.3 原始类型转字符串
原始类型转字符串就用构造函数String。我们也来过一下。
数字1转字符串,就会得到一个字符串1:
其实剩下的原始类型我们都不用看了,转字符串是最简单粗暴的,不管谁来,都直接加引号转成字符串。
undefined直接转成’undefined‘,null直接转成’null‘等等。
3. 引用类型转原始类型
我们重点介绍引用类型转原始类型,也就是对象转原始类型。
3.1 引用类型转字符串
我们想把一个对象转字符串,有什么方法呢?
我们可以直接用构造函数String。
let a = {}
console.log(String(a));
其实v8引擎在执行这段代码时,悄悄的调用了ToString方法,这个方法我们是不能使用的,它是专门用来给v8引擎使用的。其实原始类型之间的转换也是调用了这个方法,只不过原始类型之间的转换相对来说比较简单,我们就省略了。
如果是原始类型调用了ToString方法,它可以轻松拿捏。但要是引用类型的话,ToString方法就搞不定,它就会喊一个帮手辅助它。这个帮手就是ToPrimitive。它可以将引用类型转成原始类型,再拿给ToString去使用,这样ToString就能搞得定了。
let a = {}
console.log(String(a)); // ToString(a) ToPrimitive(a, string)
ToPrimitive接收到a后,它有两个参数,一个是对象a,另外一个是string,表示要将a转换成字符串。
把对象转成字符串我们还可以用这个方法:
let a = {}
console.log(a.tostring());
这个方法是写在对象原型上的,我们在类型判断那篇文章中讲过它。这两种方法的执行效果是一模一样的,底层逻辑是相同的。都是先调用了ToString发现搞不定,就传给了ToPrimitive去调用。
那我们就来看看ToPrimitive去干了什么操作。
ToString搞不定后,它就将引用类型传给了ToPrimitive,它的执行规则就是这样:
ToPrimitive(obj, String)
- 如果obj是原始类型,直接返回
- 否则,调用toString(obj),如果得到原始类型,则返回
- 否则,调用valueOf(obj),如果得到原始类型,则返回
- 否则,报错
它发现obj不是原始类型后,就执行第二步,调用toString方法。所以我们要拎清楚toString是怎么操作的。
对于一个对象{},它调用的就是对象原型上的toString方法,它会返回由 '[object' 和 class 和 ']' 组成的字符串,这个我们已经见识过了。
对于一个数组[],其实它的构造函数Array的原型上也有toString方法,它就不会去调用对象原型上的了。我们来看看数组的toString方法是怎么操作的:
空数组直接得到一个空字符串。
如果数组里面有值,直接返回由数组中元素以逗号拼接的字符串。
对于一个函数fn,它的构造函数Function的原型上也有toString方法:
对于一个Date类型,它的构造函数的原型上也有toString方法,它直接返回一段字符串类型的日期:
所以我们说toString方法有三个版本,一个是对象版本,一个是数组版本,一个是其他。
其实对于转成字符串,ToPrimitive基本上走不到第三步,大多数引用类型都会在第二步就被转换成原始类型返回了,所以在这里我们先不介绍valueOf,它在转成数字时会起作用。
所以我们可以总结,对于将一个引用类型转成字符串,我们调用了构造函数String或者调用了toString方法,它会先调用ToString方法,发现搞不定,就去调用ToPrimitive方法。ToPrimitive的执行规则如下:
ToPrimitive(obj, String)
如果obj是原始类型,直接返回
否则,调用toString(obj),如果得到原始类型,则返回
{}.toString 返回由 '[object' 和 class 和 ']' 组成的字符串
[].toString 返回由数组中元素以逗号拼接的字符串
xx.toString 直接返回 xx 字符串字面量,直接用引号引起来
否则,调用valueOf(obj),如果得到原始类型,则返回
否则,报错
3.2 引用类型转数字
我们再来看看引用类型转数字是怎么转的。
其实和转字符串差不多,当我们调用构造函数Number将一个引用类型转数字时,它会偷偷的调用ToNumber方法,发现是引用类型后搞不定,就交给ToPrimitive去调用,它就会接收两个参数,第一个是引用类型,第二个就是number,表示要将它转换成Number类型。
而对于转数字的ToPrimitive方法,它与转字符串有一点点不同,它的第二步是先调用valueOf方法。
ToPrimitive(obj, Number)
- 如果obj是原始类型,直接返回
- 否则,调用valueOf(obj),如果得到原始类型,则返回
- 否则,调用toString(obj),如果得到原始类型,则返回
- 否则,报错
那我们就着重来看看valueOf方法是怎么操作的吧。valueOf就是只在对象的原型上有,其它构造函数原型上就没有valueOf了,也就是说引用类型的valueOf都是共用同一个版本,而toString是有各自的版本。
对于一个数组调用valueOf方法,我们看看:
我们发现得到的还是一个数组,说明它搞不定数组。
对于一个对象调用valueOf方法,我们也来看:
我们发现对象它也搞不定。
对于一个函数调用valueOf方法:
函数它更搞不定。那我们要它有什么用呢,什么都搞不定。它唯一能搞定的只有包装类。
我们说b是一个字符串对象,也就是一个包装类。它唯一能搞定的只有包装类。
所以大部分对象转数字都会走到ToPrimitive的第三步。
所以我们可以总结,对于将一个引用类型转成数字,我们调用了构造函数Number,它会先调用ToNumber方法,发现搞不定,就去调用ToPrimitive方法。ToPrimitive的执行规则如下:
ToPrimitive(obj, Number)
如果obj是原始类型,直接返回
否则,调用valueOf(obj),如果得到原始类型,则返回
如果是包装类,直接能搞定返回
如果不是包装类,走第三步
否则,调用toString(obj),如果得到原始类型,则返回
否则,报错
3.3 引用类型转布尔
当我们调用Boolea函数n想把一个引用类型转布尔类型时,它就会偷偷的调用ToBoolean方法。这种是最简单的,只要你是传进来一个引用类型,直接统统转成true,这没有道理可讲的,它就是这样规定的。
我们就看这两个。只要是引用类型统统转成true。
4. 隐式类型转换的场景
学完上面的,我们差不多能搞懂在JS中是怎么进行类型转换的了。接下来,我们来看看在什么场景会发生隐式类型转换。
4.1 四则运算 + - * / %
我们着重来讲讲这个 ’+‘ 号。
4.1.1 一元 + 运算符
什么叫一元呢?就是只有一个未知项,和 + 出现的只有一个变量。比如,我们想把一个字符串’123‘转成数字来使用,可以直接这样写:
当这样来使用 + 时,我们就说 + 被当作了一元运算符来使用。
一元运算符会将其操作数转换成Number类型。
所以它会去调用ToNumber方法。这样就跳到我们上面说的了。当参数为原始类型时,ToNumber直接搞定;当参数为引用类型时,它会叫ToPrimitive来帮它。
所以如果我这样写:+[],它就会去调用ToNumber,发现搞不定就会去调用ToPrimitive。调用ToPrimitive,第二步调用valueOf搞不定,就去第三步,调用toString,而数组身上自己有toString。因为是一个空数组,所以返回一个空字符串给ToNumber。然后现在ToNumber就搞得定了,因为现在变成了原始类型,所以空字符串转数组就是0。
4.1.2 二元 + 运算符
我们再来讲讲二元运算符,也就是有两个变量。
对于二元运算符,也就是val1 + val2,它会如何进行操作呢?
它会先进行这样一个操作,先将val1 和val2传给ToPrimitive去操作,也就是lprim = ToPrimitive(val1)、rprim = ToPrimitive(val2),得到两个值,lprim和rprim。
如果 lprim 或者 rprim 是字符串,另一个值直接被 ToString()
否则,返回对 ToNumber(lprim) 和 ToNumber(rprim) 应用加法运算的结果
我们来看一个例子:null + 1 是多少?
null和1去调用ToPrimitive,因为都是原始类型直接返回,因为两边都不是字符串,所以都去转成数字类型,null转成数字类型就为0.所以最后就变成0 + 1,结果就是1了。
4.2 判断语句 if while == > < >= <= !=
我们着重来讲讲 == 。
4.2.1 ==
关于双等于,它的规则就比较多了,我们可以去官方文档上查看。
1.如果 Type(x) 与 Type(y) 相同,则
a.如果 Type(x) 为 Undefined,则返回 true。
b.如果 Type(x) 为 Null,则返回 true。
c.如果 Type(x) 为 Number,则
i.如果 x 为 NaN,则返回 false。
ii.如果 y 为 NaN,则返回 false。
iii.如果 x 与 y 是相同的 Number 值,则返回 true。
iv.如果 x 为 +0 且 y 为 −0,则返回 true。
v.如果 x 为 −0 且 y 为 +0,则返回 true。
vi.返回 false。
d.如果 Type(x) 为 String,则如果 x 和 y 是完全相同的字符序列(长度相同且相应位置的字符相同),则返回 true。否则,返回 false。
e.如果 Type(x) 为 Boolean,则如果 x 和 y 均为 True 或均为 false,则返回 true。否则,返回 false。
f.如果 x 和 y 引用同一个对象,则返回 true。否则,返回 false。
2.如果 x 为 null 且 y 为undefined,则返回 true。
3.如果 x 为 undefined 且 y 为 null,则返回 true。
4.如果 Type(x) 为 Number 且 Type(y) 为 String,则返回比较 x == ToNumber(y) 的结果。
5.如果 Type(x) 为 String 且 Type(y) 为 Number,则返回比较 ToNumber(x) == y 的结果。
6.如果 Type(x) 为 Boolean,则返回比较 ToNumber(x) == y 的结果。
7.如果 Type(y) 为 Boolean,则返回比较 x == ToNumber(y) 的结果。
8.如果 Type(x) 为 String 或 Number 且 Type(y) 为 Object,则返回比较 x == ToPrimitive(y) 的结果。
9.如果 Type(x) 为 Object 且 Type(y) 为 String 或 Number,则返回比较 ToPrimitive(x) == y 的结果。
10.返回 false。
双等于最后得到的结果一定是布尔类型
5.再谈 [] == ![]
在文章的最后,我们终于可以来解答[] == ![]的结果了.我们先来看一下它的运行结果:
结果是true.现在我们可以来分析一下结果为什么会是true.
右边的是! + [], !的作用是先将它转换成布尔类型再取反.[]转换成布尔类型,引用类型转布尔一定是true,所以取反变成false.
[] == false
然后此时符合第七条,y为布尔类型.所以去调用ToNumber,将false转换成数字类型,false转成数字类型就为0
[] == 0
然后一边是数字,一边是数组.符合第九条,于是将数组[]去调用ToPrimitive,于是会变成空字符串.
'' == 0
然后一边是字符串,一边是数字.符合第五条,将空字符串转换成数字,就为0了
最后就是 0 == 0,那不就是true了吗,所以最终结果为true
学完JS的类型转换,当面试官问我们这道题时,我们不就能侃侃而谈了吗.