`[]==![]`?一道经典面试题,带你玩转 JavaScript 类型转换

0 阅读5分钟

JavaScript 类型转换

前言,[] == ![]

这是面试官问烂的一道题,判断一个空数组是否相等于这个数组取反。新手乍一看感到荒谬:这还用问吗?你都取反了,当然应当是 false 。

我们试着在浏览器中运行 [] == ![]

image.png 结果令人大跌眼镜,居然输出了true ,也就是在浏览器 v8 引擎的眼里,这两者是相等的。敏锐的你已经察觉,== 在判断相等的过程中,一定发生了某种转换。要搞懂发生了什么,就来到今天的主题: JavaScript 类型转换

类型转换

类型转换有两种:

  1. 显示类型转换

主要是原始类型的转换

  • String(x) -> ToString(x)
  • Number(x) -> ToNumber(x)
  • Boolean(x) -> ToBoolean(x)
  1. 隐式类型转换

显式类型转换

就是明显的转换,例如 String(), Number()

原始类型之间都可以互相转化,详情可以去官方说明文档中查询。

String()

官方的描述详细而周全,想自行查阅可以跳转点击查阅官方原文

image.png

可以看到,当执行 String([value]) 时,v8会调用 ToString(value),注意这是底层规范中的抽象操作,不是我们能调用的 JS 方法,和 toString 可不是一个东西。

ToString 的运行规律是这样的

image.png 关于对象这种引用类型的我们暂且不讨论,且看原始类型。转化为字符串时:

调用输出
String(undefined)undefined
String(null)null
String(true)true
String(false)false
String('一串字符串')一串字符串
String()传数字时,需要分情况讨论

image.png

String(NaN)  // "NaN"
String(+0) Sting(-0)  // "0"
String(-1)  // "-1"
String(Infinity) // "Infinity"

Number()

同理,我们来看ToNumber() 的规则

image.png

Number()输出
undefined'NaN'
Null'0'
true'1'
false'0'

如果接收到字符串的话:

image.png

一般都是 NaN,如果是纯数字字符串则正常转为数字输出,空字符串输出0

引用类型的显式转换

当我们要把 引用类型 转换为原始类型的时候,如上表所说,需要调用一个 ToPrimitive()

引用类型 String()

let x = {}
String(x)

流程一览:

  • String(x) -> ToString(x) -> ToPrimitive(x,String)
  1. String(x)进行抽象操作ToString,发现是个对象,处理不好,就交给 ToPrimitive(x,String),在这里面调用方法 x.toString
  2. 如果得到了原始值 (primitive value) 就返回,这里直接返回 [object Object]
  3. 调用 x.valueOf() ,如果得到了 primitive value 就返回
  4. 否则报错

你说,那不对啊?反正都要 toString(),我直接用 toString()不就好了?区别在于,String()可以安全处理所有值,包括 null、undefined,而toString()不行。

注意,在 ToPrimitive(x,String)中进行了这样的操作 :

[].toString

同理,如果传进一个数组

let x = []
String(x)

首先String(x)抽象操作发现处理不了,于是交给ToPrimitive(x,String),这里有所不同,x.toString 调用的是挂载在Array.prototype 上的方法,因此不会返回[object Array],而是返回 空字符串 ''

  1. String({}) 返回 "[object Object]"
  2. String([]) 返回数组内部元素以逗号拼接得到的字符串
  3. String(xxx) 返回 "xx"

引用类型 Number()

let x = {}
Number(x)

流程上一样,细节上不同:

  • Number(x) -> ToNumber(x) -> ToPrimitive(x,Number)
  1. Number(x)进行抽象操作 ToNumber(x)处理不了,交给 ToPrimitive(x,Number)
  2. 区别来了:调用 x.valueOf()如果得到原始值,抽象操作x.ToNumber,返回
  3. 调用 x.toString(),如果得到原始值,则再抽象操作x.ToNumber转为数字
  4. 否则报错

这里传的是一个空对象,在第三步中变成了 "[object Object]",显然抽象操作后变为 NaN

如果传进去一个数组

let x = []
Number(x)

同样,经过 ToPrimitive(x,Number)处理后,变成了字符串"",空字符串抽象操作转数字变成 0

非空数组同理:

Number([6])  // 6  -> [6].toString() -> "6" -> 6
Number([1,2]) // NaN -> [1,2].toString() ->"1,2"->NaN
Number([[[1]]])// 1 -> toString -> "1" ->1

Number([[[1]]])乍一看很唬人,但是经过valueOf()处理后,可以直接把"1"提出来

补充: valueOf()

valueOf()ToPrimitive中,可以把包装类的对象则转为原始值

包装类:把原始值临时包装成对象的内置构造函数,String Number Boolean

引用类型Boolean()

所有引用类型转换为布尔类型都是 true

隐式类型转换

发生的场合

  • 四则运算
+
-
*
/
%
  • 判断语句
if
while
==
===
>
< 
>= 
<= 
!=

大多数隐式类型转换都是要转成数字 我们特别来讲讲+

一元运算符 +

  • + 作为一元运算符。例如
console.log('1') // '1'
console.log(+'1') // 1

如果放个对象呢

console.log(+{})

这就相当于

Number({})

最终得到 NaN

数组同理,console.log(+[])最终得到 0

二元运算符 +

例如

lval + raval

v8执行这样的操作

  1. lprimToPrimitive(lval)
  2. rprimToPrimitive(rprim)
  3. 于是变成lprim + rprim
  4. 如果 lprim rprim中至少有一个是字符串,则把另一个丢给 ToString()抽象操作为 字符串进行拼接,最终得到字符串
  5. 两个都不是字符串,则全部进行 ToNumber(),最后数学运算

我们回到文章开始的问题,要判断

[] == ![]

于是依照上述流程进行操作 因为 ==本质判断,是布尔类型,因此我们先转右边的,而引用类型统一变成 true:

[] == !true

然后取反

[] == false

最终我们要 Number()转换为数字判断,因此Number(false),显然是0

[] == 0

现在同理,将等式左边也转为数字判断,Number([]),抽象操作ToPrimitive([],Number),最终得到0

0 == 0

显然成立。这简直是魔法~

总结

JavaScript 类型转换主要依赖于三个抽象操作:

抽象操作触发方式核心规则
ToStringString(x)、字符串拼接原始值直接转,对象 ->ToPrimitive(x,String)-> toString->valueOf
ToNumberNumber(x)、四则运算、==原始值直接转;对象 → ToPrimitive(x, Number) → 先 valueOf,后 toString
ToBooleanBoolean(x)!if引用类型一律 true0 NaN "" null undefinedfalse