前言
学习 JavaScript 类型转换时,有些表达式的结果很容易让人疑惑:
为什么 \[\] == !\[\] 的结果是 true?
为什么 \[\] + {} 得到的却是字符串?
下面我将从显式转换讲起,再用这些规则详细拆解整个转换过程。
一、 == VS ===
== 在判断是否相等的过程中可能发生隐式类型转换,而 === 不会。用两组相同的操作数对照来看:
js
console.log(1 == '1') // true
console.log(0 == false) // true
使用 == 时,字符串 '1' 和布尔值 false 分别转换为数字 1 和 0,因此两次比较都得到 true。
js
console.log(1 === '1') // false
console.log(0 === false) // false
换成 === 后,数字与字符串、数字与布尔值的类型不同,两次比较都直接得到 false。
二、显式类型转换
显式转换,就是主动调用 String()、Number()、Boolean()。它们对应的规范规则如下:
| 调用方式 | 对应规则 | 转换目标 |
|---|---|---|
| String(x) | ToString(x) | 字符串 |
| Number(x) | ToNumber(x) | 数字 |
| Boolean(x) | ToBoolean(x) | 布尔值 |
2.1 ToPrimitive:将引用类型转为一个原始值
对象转字符串、转数字,都需要先经过 ToPrimitive。它是规范中描述转换过程的抽象操作,作用是取得一个原始值,供后续转换使用。
ToPrimitive(O, hint) 中,O 是待转换的值,hint 是转换时的类型提示。如果输入已经是原始值,就直接返回;如果是对象,则需要尝试获取它的原始值。

hint 决定方法的尝试顺序,不决定最终返回值的类型。 按照下图的规则,两种提示对应的顺序如下:
| 类型提示 | 先尝试 | 再尝试 |
|---|---|---|
| String | toString() | valueOf() |
| Number | valueOf() | toString() |
每一步都先检查方法是否可调用。可以调用就检查返回值:得到原始值,立即结束;仍是对象,就继续尝试下一个方法。两种方法都无法提供原始值,才抛出 TypeError。
这里有两个方法需要分清:
- toString():toString方法可以分为以下三种情况,默认情况下,普通对象返回字符串 "object Object";数组把元素用逗号连接起来,空数组得到空字符串;其他则直接用引号引起来。
js
1. {}.toString() // 返回 [object Object]
2. [].toString() // 返回数组内部的元素以逗号拼接得到的字符串
3. 其他xx.toString() // 返回xx的字符串表示: 'xx'(直接用引号引起来)
- valueOf():数字、字符串、布尔值的包装对象可以通过它取出原始值;普通对象和数组默认返回自身,因此还需要继续尝试 toString()。
例如,对普通对象使用 Number 提示,ToPrimitivehui会先调用 valueOf() ,但由于得到的仍是对象,会再调用 toString() 得到字符串 "object Object"。这个字符串已经是原始值,ToPrimitive 的工作便结束了;是否继续转成数字,要看后续的 ToNumber。
没有提供 hint 时,通常按 Number 处理;Date 对象按 String 处理。下面是对应规则的中文说明:

2.2 ToString()
先看几个直接的转换:
js
console.log(String()) // ''
console.log(String(undefined)) // 'undefined'
console.log(String(null)) // 'null'
console.log(String(true)) // 'true'
console.log(String(false)) // 'false'
console.log(String(0)) // '0'
console.log(String(NaN)) // 'NaN'
console.log(String(Infinity)) // 'Infinity'
String() 不传参数会得到空字符串;上面的其他原始值则转换为对应的字符串表示。
引用类型转字符串时,先执行带有 String 提示的 ToPrimitive,再把得到的原始值交给 ToString。

以普通对象 {a: 1} 为例:
- String() 按 ToString 规则处理对象,先执行带有 String 提示的 ToPrimitive。
- 优先调用 toString(),得到原始值 "object Object",不再尝试 valueOf()。
- 将这个原始值交给 ToString;它已经是字符串,直接返回。
2.3 ToNumber()
js
console.log(Number()) // 0
console.log(Number(undefined)) // NaN
console.log(Number(null)) // 0
console.log(Number(true)) // 1
console.log(Number(false)) // 0
console.log(Number('123')) // 123
console.log(Number('-123')) // -123
console.log(Number('123.456')) // 123.456
console.log(Number('000123')) // 123
console.log(Number('000123a')) // NaN
console.log(Number(Infinity)) // Infinity
这里有两处值得注意:null 转数字得到 0,undefined 得到 NaN;字符串要转数字也必须整体符合数字格式,否则会转成NaN n。

引用类型转数字时,先执行带有 Number 提示的 ToPrimitive,再对结果执行 ToNumber。仍以普通对象 {a: 1} 为例:
- 优先调用 valueOf(),返回对象自身,继续尝试。
- 调用 toString(),得到原始值 "object Object"。
- ToNumber 将这个字符串转成数字;它不符合数字格式,结果为 NaN。
因此,引用转字符串和转数字时,不仅尝试方法的顺序不同,拿到原始值之后要执行的转换也不同。
2.4 ToBoolean
对象转布尔值不经过 ToPrimitive,引用类型都转为 true。 空数组、空对象也一样。
在图中列出的类型里,undefined、null、false、+0、-0、NaN 和空字符串会得到 false。

js
console.log(Boolean()) // false
console.log(Boolean(undefined)) // false
console.log(Boolean(null)) // false
console.log(Boolean(0)) // false
console.log(Boolean('')) // false
console.log(Boolean('0')) // true
console.log(Boolean('false')) // true
console.log(Boolean('true')) // true
console.log(Boolean(123)) // true
console.log(Boolean(NaN)) // false
console.log(Boolean(Infinity)) // true
字符串判断的是是否为空。'0' 和 'false' 都有内容,所以结果都是 true。
三、隐式类型转换
隐式转换发生在运算或判断过程中,具体转换目标由当前操作决定:
- 四则运算:
+-*/% - 判断语句:
ifwhileswitch==>=<=!=><
注意:
==>=<=!=><这些符号的两边都会转为 Number类型,但最终整个式子还是 Boolean类型。!运算符的优先级最高,会先将!后面的转为 Boolean类型,再取反。
3.1 '+' 作为一元运算符:转成 Number
js
console.log(+[]) // 0
一元 + 要求数字,因此空数组先按 Number 提示转换为原始值:
- valueOf() 返回数组自身,仍然是对象。
- 继续调用 toString(),得到空字符串 ""。
- 空字符串再转成数字 0。
3.2 '+' 作为二元运算符:先得到原始值,再决定转 String 还是 Number
对于 x + y,规则分为以下步骤:
- 令 lprim 为 ToPrimitive(x)。 此时ToPrimitive(x)
- 令 rprim 为 ToPrimitive(y) 的结果。
- lprim + rprim
- 如果 lprim 或者 rprim 有一个是字符串,就将另一个也转成字符串进行拼接
- 否则全部转 number 进行相加

js
console.log([] + {}) // "[object Object]"
数组和普通对象的 valueOf() 都返回自身,接着尝试 toString():左边得到 "",右边得到 "object Object"。此时进行字符串拼接,结果自然就是 "object Object"。
3.3 回到 \[\] == !\[\]:逐步看清转换
js
console.log([] == ![]) // true
在这个表达式中,! 比 == 优先执行。右边的空数组先转成 true,取反后得到 false。
后续转换如下:
| 当前表达式 | 下一步 |
|---|---|
| \[\] == false | 布尔值 false 转成数字 0 |
| \[\] == 0 | 数组转成原始值,得到空字符串 |
| '' == 0 | 空字符串转成数字 0 |
| 0 == 0 | 比较结果为 true |
左右两边虽然都是空数组,参与的操作却不同:右边先做布尔转换并取反,左边在相等比较中先转成原始值,最终经过一系列转换得到true。