
在 JavaScript 的世界里,数据类型之间的转换无处不在。即使你没有主动显式地去转换,但 JavaScript 在私底下“偷偷地”帮我们做了很多类型转换的工作。那么,它们究竟是按照什么规则去转换的呢?我们试图在本文中找到答案,来解开谜底。
ECMAScript 是负责制定标准的,而 JavaScript 则是前者的一种实现。
在 ECMAScript 标准中定义一组转换抽象操作,常见的抽象操作(Abstract Operation)有 ToPrimitive、ToBoolean、ToNumber、ToString、ToObject 等等。
一、ToPrimitive
1. ToPrimitive
在 ECMAScript 标准中,使用 ToPrimitive 抽象操作将引用类型转换为原始类型。(详情看 #sec-7.1.1)

ToPrimitive(input[, PerferredType])
参数 input 是文章开头提到的 8 种数据类型的值(Undefined、Null、Boolean、String、Symbol、Number、BigInt、Object)。参数 PreferredType 是可选的,表示要转换到的原始值的预期类型,取值只能是字符串 "default"(默认)、"string"、"number" 之一。
ToPrimitive 操作,可概括如下:
-
input是 ECMAScript 语言类型的值。 - 如果
input是引用类型,那么- 如果没有传递
PreferredType参数,使hint等于"default"。 - 如果参数
PreferredType提示 String 类型,使hint等于"string"。 - 否则,参数
PreferredType提示 Number 类型,使hint等于"number"。 - 使
exoticToPrim等于GetMethod(input, @@toPrimitive)的结果(大致意思是,获取input对象的@@toPrimitive属性值,并将其赋给exoticToPrim)。 - 如果
exoticToPrim不等于undefined(即input对象含@@toPrimitive属性),那么- 使
result等于Call(exoticToPrim, input, « hint »)的结果(大致意思是,执行@@toPrimitive方法,即exoticToPrim(hint))。 - 如果
result不是引用类型,则返回result。 - 否则抛出
TypeError类型错误。
- 使
- 如果
hint是"default",则将hint设为"number"。 - 返回
OrdinaryToPrimitive(input, hint)。
- 如果没有传递
- 返回
input(即原始类型的值直接返回)。
注意:当不带
hint去调用 ToPrimitive 抽象操作时,通常它的行为就像hint是Number类型一样。但是,(派生)对象可以通过定义@@toPrimitive方法来替代此行为。在本规范中定义的对象里,只有Date对象(详情看 #sec-20.4.4.45)和Symbol对象(详情看 #sec-19.4.3.5)会覆盖默认的 ToPrimitive 行为。
其他:
- 以上提到的
@@toPrimitive方法,即属性名称为[Symobl.toPrimitive]的方法。Date对象的@@toPrimitive方法定义:当hint为"default"时,会使hint等于"string"。所以这也是Date对象转换为原始值时,会先调用instance.toString()方法,而不是instance.valueOf()方法的原因。- 目前 JavaScript 的内置对象中,含有
@@toPrimitive方法的,只有Date和Symbol对象。
用口水话再总结一下,如下(哈哈):
- 如果
input是原始类型,直接返回input(不做转换操作)。 - 如果参数
PreferredType是 String(Number)类型,那么使得hint等于"string"("number"),否则hint等于默认的"default"。 - 如果
input中存在@@toPrimitive属性(方法),若@@toPrimitive方法的返回值为原始类型,则 ToPrimitive 的操作结果就是该返回值,否则抛出TypeError类型错误。 - 如果经过以上步骤之后
hint是"default",则使hint等于"number"。 - 返回
OrdinaryToPrimitive(input, hint)操作的结果。
提醒:
关于
Date对象的Date.prototype[@@toPrimitive]内部方法实现,其实是将hint为"default"的情况改为"string",然后执行第 5 步的OrdinaryToPrimitive操作。(详情看 #sec-20.4.4.45)关于
Symbol对象的Symbol.prototype[@@toPrimitive]内部方法实现,如果传递给该方法的是一个Symbol类型的值,则直接返回该值。如果该值是引用类型,且含有属性[[SymbolData]],而且该属性值为Symbol类型,则返回该属性值,否则会抛出TypeError类型错误。(详情看 #sec-19.4.3.5)
那么 OrdinaryToPrimitive 的操作是怎样的呢?我们接着往下看...
2. OrdinaryToPrimitive
详情看:#sec-7.1.11

OrdinaryToPrimitive(O, hint)
参数 O 为引用类型。参数 hint 为 String 类型,其值只能是字符串 "string"、"number" 之一。
(官话)OrdinaryToPrimitive 操作,可概括如下:
-
O是引用类型。 -
hint是 String 类型,且hint的值只能是"string"或"number"之一。 - 如果
hint为"string",使methodNames等于« "toString", "valueOf" »(其中«»表示规范中的 List,类似于数组)。 - 如果
hint为"number",使methodNames等于« "valueOf", "toString" »。 - 遍历
methodNames,使name等于每个迭代值,并执行:- 使
method等于Get(O, name)(即获取对象O的name属性,相当于获取对象的toString或valueOf属性,具体执行顺序视hint而定)。 - 如果
IsCallable(method)结果为true,那么:- 使
result等于Call(method, O)结果(即调用method()方法)。 - 如果
result为原始类型,则返回result。
- 使
- 使
- 抛出
TypeError类型错误。
其中
IsCallable(argument)操作,大致内容是:当参数argument为引用类型且argument对象包含内部属性[[Call]]时返回true, 否则返回false。话句话说,就是用于判断是否为函数。
(口水话)再总结一下:
- 当经过 ToPrimitive 操作,然后执行
OrdinaryToPrimitive(input, hint)操作,那么步骤如下: - 如果
hint为"string",它会先调用input.toString()方法,- 若
toString()结果为原始类型,则直接返回该结果。 - 否则,继续调用
input.valueOf()方法,若结果为原始类型,则返回该结果,否则抛出TypeError类型错误。
- 若
- 若
hint为"number",它先调用input.valueOf()方法,- 若
valueOf()结果为原始类型,则直接返回该结果。 - 否则,继续调用
input.toString()方法,若结果为原始类型,则返回该结果,否则抛出TypeError类型错误。
- 若
到这里,已经完整地讲述了 ToPrimitive 操作的全部过程。文笔不太好,我不知道你们有没看明白,倘若仍有疑惑,请反复斟酌或直接查看 ECMAScript 标准。
3. 一些示例
-
-、*、/、%这四种操作符都会把符号两边的操作数先转换为数字再进行运算。 -
+的作用可以是数值求和,也可以是字符串拼接。- 若符号两边操作数都是数字,则进行数字运算。
- 若符号一边是字符串,则会把另一端转换为字符串进行拼接操作。
一元加运算符
+(unary plus)是将操作数转换为数字的最快且首选的方式,因为它不对该数字执行任何其他运算。区分一元加运算符(
+)和算术运算符(+)的方法,就是前者只有一个操作数,而后者是两个操作数。
// 运算符: x + y
// Number + Number -> 数字相加
1 + 2 // 3
// Boolean + Number -> 数字相加
true + 1 // 2
// Boolean + Boolean -> 数字相加
false + false // 0
// Number + String -> 字符串连接
5 + 'foo' // "5foo"
// String + Boolean -> 字符串连接
'foo' + false // "foofalse"
// String + String -> 字符串连接
'foo' + 'bar' // "foobar"
const obj = {
[Symbol.toPrimitive]: hint => {
if (hint === 'number') {
return 1
} else if (hint === 'string') {
return 'string'
} else {
return 'default'
}
}
}
+obj // 1 hint is "number"
`${obj}` // "string" hint is "string"
obj + '' // "default" hint is "default"
obj + 1 // "default1" hint is "default"
Number(obj) // 1 hint is "number"
String(obj) // "string" hint is "string"
二、ToBoolean
将一个操作数转换为布尔值,这应该是最简单的了。(详情看 #sec-7.1.2)

所以总结下来就是:
| 操作数 | 结果 |
|---|---|
undefined、null、false、+0、-0、NaN、''、0n
|
false |
| 除以上这些(falsy)值之外 | true |
在 JavaScript 中,如果一个操作数 argument 通过 ToBoolean(argument) 操作后被转换为 true,那么这些操作数称为真值(truthy),否则为虚值(falsy)。
// 转换为 Boolean 值的两种方式
!!x
Boolean(x)
三、ToNumber
将一个操作数转换为数字值。(详情看 #sec-7.1.4)

| 参数类型 | 结果 |
|---|---|
| Undefined | NaN |
| Null | +0 |
| Boolean |
true 转换为 1,false 转换为 +0。 |
| Number | 直接返回,不做类型转换。 |
| String | 1. 纯数字的字符串转换为相应的数字; 2. 空字符串 '' 转为 +0;3. 否则为 NaN;其中 0x 开头的字符串被当成 16 进制。 |
| Symbol | 无法转换,抛出 TypeError 错误。 |
| BigInt | 无法转换,抛出 TypeError 错误。 |
| Object | 两个步骤: 1. 将引用类型转化为原始值 ToPrimitive(argument, 'number');2. 转化为原始值后,进行 ToNumber(primValue) 操作,即按上面的类型转换。 |
需要注意的是
Number(undefined)结果为NaN,而Number(null)结果为0。- 含有前导和尾随空白符(
\n、\r、\t、\v、\f)的字符串,在转换为数字类型的时候空白符会被忽略。- 上面也提到过,使用一元加运算符(
+) 是将其他类型转换为数值的常用方式。
Number(undefined) // NaN
Number(null) // 0
'\n 123 \t' == 123 // true
+'string' // NaN
+true // 1
+[] // 0
+{} // NaN
四、ToString
将一个操作数转换为字符串类型的值。(详情看 #sec-7.1.17)

| 参数类型 | 结果 |
|---|---|
| Undefined | undefined |
| Null | null |
| Boolean |
true 转换为 "true",false 转换为 "false"。 |
| Number | 1. NaN 转换为 "NaN";2. +0 或 -0 转换为 "0";3. 其中 Infinity 和 -Infinity 分别转换为 "Infinity"、"-Infinity";4. 若 x 是小于 0 的负数,则返回 "-x";若 x 是大于 0 的正数,则返回 "x";5. 其他不常用的数值,请看 #sec-6.1.6.1.20。 |
| String | 直接返回,不做类型转换。 |
| Symbol | 无法转换,抛出 TypeError 错误。 |
| BigInt |
10n 转换为 "10"。 |
| Object | 两个步骤: 1. 将引用类型转化为原始值 ToPrimitive(argument, 'string');2. 转化为原始值后,进行 ToString(primValue) 操作,即按上面的类型转换。 |
需要注意的是,Symbol 原始值不能转换为字符串,只能将其转换成对应的包装对象,再调用
Symbol.prototype.toString()方法。
// 下面会导致 Symbol('foo') 进行隐式转换,即 ToString(Symbol),按以上规则,是会抛出异常的
console.log(Symbol('foo') + 'bar' ) // TypeError: Cannot convert a Symbol value to a string
// Symbol('foo') 结果是 Symbol 的原始值,再调用其包装对象的属性时,会自动转化为包装对象再调用其 toString() 方法
console.log(Symbol('foo').toString() + 'bar' ) // "Symbol(foo)bar"
抛一个有趣的问题:
// 运行出错
var name = Symbol() // TypeError: Cannot convert a Symbol value to a string
// 正常运行,不会抛出错误
let name = Symbol()
// 为什么呢 ❓❓❓
答案我不写了,感兴趣的可以自行搜索。对此有疑问的可以先看下文章:关于 var、let 的顶层对象的属性。
五、ToObject
将一个操作数转换为引用类型的值。(详情看 #sec-7.1.18)

| 参数类型 | 结果 |
|---|---|
| Undefined | 无法转换,抛出 TypeError 错误。 |
| Null | 无法转换,抛出 TypeError 错误。 |
| Boolean | 返回 new Boolean(argument)。 |
| Number | 返回 new Number(argument)。 |
| String | 返回 new String(argument)。 |
| Symbol | 返回 Object(Symbol(argument))。 |
| BigInt | 返回 Object(BigInt(argument))。 |
| Object | 直接返回,不做类型转换。 |
需要注意都是,JavaScript 内置的
Symbol、BigInt对象不能使用new关键字去创建实例对象,只能通过Object()函数来创建一个包装对象(wrapper object)。
// 错误示例
const sym = new Symbol() // TypeError: Symbol is not a constructor
// 正确示例
const sym = Symbol()
console.log(typeof sym) // "symbol"
const symObj = Object(sym)
console.log(typeof symObj) // "object"
// BigInt 同理
需要注意的是,从 ES6 开始围绕原始数据类型创建一个显式包装器对象不再被支持。但由于遗留原因,现有的原始包装器对象(如
new Boolean、new Number、new String)仍可使用。这也是 ES6+ 新增的 Symbol、BigInt 数据类型无法通过new关键字创建实例对象的原因。