面试官:[] == ![],返回true还是false,为什么?

312 阅读12分钟

今天我们来讲讲JS中的类型转换机制。我们在面试中大概率碰到这道题:[] == ![]。请问它返回的是true还是false,为什么?

想要搞清楚这道题的原理,我们就得搞清楚JS中是怎么进行类型转换的。等你看完这篇文章后,你就能从容应对面试官了。

1. == VS ===

我们先来看一下双等于和三等于的区别。

对于1 === 1,我们知道,这两个一定相等,因为都是数字1。对于'1' === '1',我们也知道,这两个一定相等,因为都是字符串1。

那对于'1' === 1,它们相等吗?这类型都不一样,字符串1怎么可能等于数字1呢,肯定不相等。确实不相等,那'1' == 1,返回的是true还是false呢?

image.png

这两个是相等的。这是因为在双等于号中发生了隐式类型转换,它会把字符串1转换成数字1去比较是否相等。而三等于号不会发生隐式类型转换,如果两边的值类型都不相等,那两边一定不相等。

所以我们有个结论:

== 会发生隐式类型转换,所以只会判断值是否相等

=== 不会发生隐式类型转换,意味着就判断值和类型是否相等

2. 原始类型之间的转换

我们先来聊聊原始类型之间的转换。原始类型的转换比较简单,我们简单过一遍。我们重点介绍引用类型转原始类型。

2.1 原始类型转布尔类型

我们想把一个原始类型转布尔类型,应该怎么转。用它的构造函数转。

我们来过一遍原型类型转成布尔类型会是什么结果。

undefined转布尔:

image.png

null转布尔:

image.png

数字1转布尔:

image.png

数字0转布尔:

image.png

NaN转布尔:

image.png

所以在数字类型中,只有0和NaN转布尔为false,其它全为true。

字符串转布尔:

image.png

空字符串转布尔:

image.png

Bigint和Symbol类型因为是后来新增的,它们一定不会发生类型转换的,所以我们不用管。

原始类型转布尔我们已经看完了。

2.2 原始类型转数字

原始类型转数字用构造函数Number转。

字符串1转数字:

image.png

直接变成数字1,很好理解。

空字符串转数字:

image.png

字符串hello转数字:

image.png

undefined转数字:

image.png

null转数字:

image.png

true转数字:

image.png

false转数字:

image.png

原始类型转数字我们也过了一遍。

2.3 原始类型转字符串

原始类型转字符串就用构造函数String。我们也来过一下。

数字1转字符串,就会得到一个字符串1:

image.png

其实剩下的原始类型我们都不用看了,转字符串是最简单粗暴的,不管谁来,都直接加引号转成字符串。

undefined直接转成’undefined‘,null直接转成’null‘等等。

3. 引用类型转原始类型

我们重点介绍引用类型转原始类型,也就是对象转原始类型。

3.1 引用类型转字符串

我们想把一个对象转字符串,有什么方法呢?

我们可以直接用构造函数String。

let a = {}
console.log(String(a));

image.png

其实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方法是怎么操作的:

image.png

空数组直接得到一个空字符串。

image.png

如果数组里面有值,直接返回由数组中元素以逗号拼接的字符串。

对于一个函数fn,它的构造函数Function的原型上也有toString方法:

image.png

对于一个Date类型,它的构造函数的原型上也有toString方法,它直接返回一段字符串类型的日期:

屏幕截图 2024-12-01 213730.png

所以我们说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方法,我们看看:

image.png

我们发现得到的还是一个数组,说明它搞不定数组。

对于一个对象调用valueOf方法,我们也来看:

image.png

我们发现对象它也搞不定。

对于一个函数调用valueOf方法:

image.png

函数它更搞不定。那我们要它有什么用呢,什么都搞不定。它唯一能搞定的只有包装类。

image.png

我们说b是一个字符串对象,也就是一个包装类。它唯一能搞定的只有包装类。

所以大部分对象转数字都会走到ToPrimitive的第三步。

所以我们可以总结,对于将一个引用类型转成数字,我们调用了构造函数Number,它会先调用ToNumber方法,发现搞不定,就去调用ToPrimitive方法。ToPrimitive的执行规则如下:

ToPrimitive(obj, Number)

  • 如果obj是原始类型,直接返回

  • 否则,调用valueOf(obj),如果得到原始类型,则返回

如果是包装类,直接能搞定返回

如果不是包装类,走第三步

  • 否则,调用toString(obj),如果得到原始类型,则返回

  • 否则,报错

3.3 引用类型转布尔

当我们调用Boolea函数n想把一个引用类型转布尔类型时,它就会偷偷的调用ToBoolean方法。这种是最简单的,只要你是传进来一个引用类型,直接统统转成true,这没有道理可讲的,它就是这样规定的。

image.png

image.png

我们就看这两个。只要是引用类型统统转成true。

4. 隐式类型转换的场景

学完上面的,我们差不多能搞懂在JS中是怎么进行类型转换的了。接下来,我们来看看在什么场景会发生隐式类型转换。

4.1 四则运算 + - * / %

我们着重来讲讲这个 ’+‘ 号。

4.1.1 一元 + 运算符

什么叫一元呢?就是只有一个未知项,和 + 出现的只有一个变量。比如,我们想把一个字符串’123‘转成数字来使用,可以直接这样写:

image.png

当这样来使用 + 时,我们就说 + 被当作了一元运算符来使用。

一元运算符会将其操作数转换成Number类型。

所以它会去调用ToNumber方法。这样就跳到我们上面说的了。当参数为原始类型时,ToNumber直接搞定;当参数为引用类型时,它会叫ToPrimitive来帮它。

所以如果我这样写:+[],它就会去调用ToNumber,发现搞不定就会去调用ToPrimitive。调用ToPrimitive,第二步调用valueOf搞不定,就去第三步,调用toString,而数组身上自己有toString。因为是一个空数组,所以返回一个空字符串给ToNumber。然后现在ToNumber就搞得定了,因为现在变成了原始类型,所以空字符串转数组就是0。

image.png

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了。

image.png

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.再谈 [] == ![]

在文章的最后,我们终于可以来解答[] == ![]的结果了.我们先来看一下它的运行结果:

image.png

结果是true.现在我们可以来分析一下结果为什么会是true.

右边的是! + [], !的作用是先将它转换成布尔类型再取反.[]转换成布尔类型,引用类型转布尔一定是true,所以取反变成false.

[] == false

然后此时符合第七条,y为布尔类型.所以去调用ToNumber,将false转换成数字类型,false转成数字类型就为0

[] == 0

然后一边是数字,一边是数组.符合第九条,于是将数组[]去调用ToPrimitive,于是会变成空字符串.

'' == 0

然后一边是字符串,一边是数字.符合第五条,将空字符串转换成数字,就为0了

最后就是 0 == 0,那不就是true了吗,所以最终结果为true

学完JS的类型转换,当面试官问我们这道题时,我们不就能侃侃而谈了吗.