在 JavaScript 的世界里,数据类型之间的转换无处不在。即使你没有主动显式地去转换,但 JavaScript 在私底下“偷偷地”帮我们做了很多类型转换的工作。那么,它们究竟是按照什么规则去转换的呢?我们试图在本文中找到答案,来解开谜底。
ECMAScript 是负责制定标准的,而 JavaScript 则是前者的一种实现。
在 ECMAScript 标准中定义一组转换抽象操作,常见的抽象操作(Abstract Operation)有 ToPrimitive
、ToBoolean
、ToNumber
、ToString
、ToObject
等等。
一、ToPrimitive
1. ToPrimitive
在 ECMAScript 标准中,使用 ToPrimitive 抽象操作将引用类型转换为原始类型。(详情看 #sec-7.1.1)
参数 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
(即原始类型的值直接返回)。
用口水话再总结一下,如下(哈哈):
- 如果
input
是原始类型,直接返回input
(不做转换操作)。 - 如果参数
PreferredType
是 String(Number)类型,那么使得hint
等于"string"
("number"
),否则hint
等于默认的"default"
。 - 如果
input
中存在@@toPrimitive
属性(方法),若@@toPrimitive
方法的返回值为原始类型,则 ToPrimitive 的操作结果就是该返回值,否则抛出TypeError
类型错误。 - 如果经过以上步骤之后
hint
是"default"
,则使hint
等于"number"
。 - 返回
OrdinaryToPrimitive(input, hint)
操作的结果。
那么 OrdinaryToPrimitive
的操作是怎样的呢?我们接着往下看...
2. OrdinaryToPrimitive
详情看:#sec-7.1.11
参数 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
类型错误。
(口水话)再总结一下:
- 当经过 ToPrimitive 操作,然后执行
OrdinaryToPrimitive(input, hint)
操作,那么步骤如下: - 如果
hint
为"string"
,它会先调用input.toString()
方法,- 若
toString()
结果为原始类型,则直接返回该结果。 - 否则,继续调用
input.valueOf()
方法,若结果为原始类型,则返回该结果,否则抛出TypeError
类型错误。
- 若
- 若
hint
为"number"
,它先调用input.valueOf()
方法,- 若
valueOf()
结果为原始类型,则直接返回该结果。 - 否则,继续调用
input.toString()
方法,若结果为原始类型,则返回该结果,否则抛出TypeError
类型错误。
- 若
3. 一些示例
-
-
、*
、/
、%
这四种操作符都会把符号两边的操作数先转换为数字再进行运算。 -
+
的作用可以是数值求和,也可以是字符串拼接。- 若符号两边操作数都是数字,则进行数字运算。
- 若符号一边是字符串,则会把另一端转换为字符串进行拼接操作。
// 运算符: 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 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('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()
// 为什么呢 ❓❓❓
五、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 | 直接返回,不做类型转换。 |
// 错误示例
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 同理