深入类型转换
前言
ES规范中定义了很多类型转换的函数,用于将任意的值转换为指定类型,其中重要的有以下几个:
ToBoolean
转换为布尔类型ToNumber
转换为数值类型ToString
转换为字符串类型ToPrimitive
转换为基本数据类型
以上几种类型转换适用范围非常广,可以说我们无时无刻不在跟它们打交道:
// ToBoolean
if (3) {} // true
if ([]) {} // true
if ('') {} // false
// ToNumber
const a = '3' - 1 // 2
// ToString
'1' + 'hello' // 1hello
[] + 'hello' // hello
{} + 'hello' // [object Object]hello
// ToPrimitive
[1] == '1' // true
[1] == 1 // true
[1] == true // true
如果你对上述代码执行的结果感到意外和困惑,请继续看下去,下面我将逐个解析每个转换函数执行的内在逻辑,最后将所有函数串联起来,深入理解JavaScript类型转换的内涵。
ToBoolean
ToBoolean对应的API是 Boolean()
,它的规则非常简单,只要是以下值,便为 false
(一般称为falsy),其他均为 true
:
- undefined
- null
- +0, -0, 0n, NaN
- ''
注意
有趣的是,document.all
这个对象进行布尔转换时的结果为 false
。(可在控制台打印 Boolean(document.all)
试试)
ES规范中特意讲了这个特殊情况:https://tc39.es/ecma262/multipage/additional-ecmascript-features-for-web-browsers.html#sec-IsHTMLDDA-internal-slot-to-boolean
场景
以下这些场景都会触发转换布尔值操作:
if (test)
while (test)
for(;test;)
if/while/for语句的条件判断foo ? a : b
三元表达式的条件语句a || b
a && b
两侧的条件语句
让我们用一些例子来练练手:
if ([]) {} // true
if ({}) {} // true
if ('') {} // false
({}) ? 1 : 2 // 1
0 || [] // []
1 || [] // 1
0 && [] // 0
1 && [] // []
参考
ToPrimitive
该函数用于将 非基本数据类型 的值转换为基本数据类型值,因为JavaScript中所有非基本数据类型都是对象,所以其实该函数就是将对象转换为基本数据类型值。
ToPrimitive
的规则是:
- 如果被转换的值是基本数据类型,则直接返回该值
- 如果该值是对象,则需要先分析该对象的转换偏好是数值还是字符串
- 根据转换偏好调用对应的函数,如果偏好是数值则先调用
valueOf
,如果是字符串则先调用toString
- 如果两个函数调用的结果有一个是基本数据类型值,则返回,如果一个都没有,则报错
有两个需要注意的点,不管偏好是什么,valueOf
与 toString
都可能会被调用,这取决于是否返回了基本数据类型值。举个例子:
将一个空对象 {}
转换为基本数据类型,因为空对象的转换偏好是字符串,则首先会调用 toString
,如果调用的结果不是基本数据类型,则会继续调用 valueOf
函数 ,直到获得一个基本数据类型的值。如果都没有,则会报错。请看下方代码:
const o = {
toString() {
return {}
},
valueOf() {
return 1
}
}
const foo = o + 1
console.log(foo) // 2
const foo = o + 1
这行代码会触发对象 o
的 ToPrimitive
操作,因为 o
是一个对象,并且转换偏好是字符串,因此会先调用 toString
,但是 toString
返回的结果不是一个基本数据类型值,则会继续调用 valueOf
,结果是基本数据类型 - 数值类型的1 ,则最后返回了1,导致 foo
的结果是2。
将这个例子稍微改动一下:
const o = {
toString() {
return {}
},
valueOf() {
return {}
}
}
const bar = o + 1
console.log(bar) // TypeError: Cannot convert object to primitive value
当两次尝试将对象 o
转换为基本数据类型值都失败后,最终抛出错误。
场景
我们还是从原理理解问题,因为 ToPrimitive
的本质是将对象转换为基本数据类型,因此 所有需要将对象转换为基本数据类型值的地方都需要它。这句话好像一句废话,我们把它更具化一些:当对象与基本数据类型同时出现,并且需要进行赋值、比较的时候,都需要它。
例如:
[1] == 1
使用==
进行比较时,会触发隐式类型转换,[1]
最终其实被转换为了1
,因此结果为true
[1] + 1
[1] - 1
使用运算操作时,同样会触发隐式转换
参考
ToString
该函数用于将任意值转换为字符串类型的值,它的规则如下所示:
- 如果传入参数为一个字符串,返回这个参数
- 如果参数为
undefined
null
true
false
,分别返回'undefined'
'null'
'true'
'false'
- 如果参数类型为数值(number/bigint)类型,调用数值包装对象的
toString()
方法返回字符串 - 如果参数是对象,则调用
ToPrimitive
获得基本数据类型值,如果返回的不是基本数据类型值,则报错(注意!不会递归调用ToString
) - 如果返回的是基本数据类型值,则继续调用该值的
ToString
操作
ToNumber
该函数用于将任意值转换为数值类型的值,规则如下:
- 如果参数为数值类型,返回这个参数
- 如果参数类型为
symbol
或bigint
,则抛出错误 - 如果参数为
undefined
,则返回NaN
- 如果参数为
null
,返回0
- 如果参数为
true
或false
,分别返回1
或0
- 如果参数为字符串类型,则调用
StringToNumber
将字符串转为数值 - 如果参数为对象类型,则调用
ToPrimitive
将对象转为基本数据类型值 - 如果
ToPrimitive
返回的结果还是一个对象,则抛出错误 - 否则说明返回的是基本数据类型值,继续调用
ToNumber
来看点例子:
Number(undefined) // NaN
Number(null) // 0
Number(true) // 1
Number(false) // 0
Number(Symbol.iterator) // TypeError
Number('123') // 123
Number('hello') // NaN
Number([]) // 0
Number([1]) // 1
Number([1, 2]) // NaN
Number({}) // NaN
Number({
valueOf() { return 1; }
}) // 1
1 - null // 1
1 + null // 1
1 - [] // 1
1 - {} // NaN
1 - [1] // 0
场景
- 数值运算时,例如
2 - '1'
会将字符串'1'
转换为数值的1
参考
深入理解类型转换
JavaScript的类型转换分为两种类型:
- 基本数据类型互相转换,例如:字符串 -> 数值,数值 -> 布尔
- 对象转基本数据类型,例如:对象 -> 数值,对象 -> 字符串
看下面的例子:
[1] - 1 // 0
不同的编程语言处理上述代码的结果是不同的:
- 在一些强类型语言中,将数组与数值进行运算是不合法的,会在编译阶段就发生错误
- 一些弱类型的语言可能无法再编译期间发现错误,但是会在运行时判定操作不合法而抛出错误
- 在JavaScript这种语言中,运行时会进行隐式类型转换,尽可能不抛出错误
在JavaScript中,数组 [1]
会先进行 ToPrimitive
操作,试图将该数组转换为一个基本数据类型值,转换时,先调用 valueOf
函数,得到了数组自身 [1]
,由于返回值还是一个对象,因此会调用 toString
函数,最后得到字符串的 '1'
(数组的 toString
会将元素使用 ,
进行拼接),所以,上面的代码相当于这行代码:
'1' - 1
接下来,将进行两个基本数据类型值的减法运算,由于字符串没有减法运算,因此会将字符串转成数值,再与数值类型的值相减。字符串 '1'
被转换为数值的 1
,最后,1 - 1
得到了 0
。
以上例子只是举了一个减法操作的场景,在JavaScript中,不同的场景触发的隐式类型转换是有差异的 ,但都脱离不了 ToBoolean
/ ToNumber
/ ToString
/ ToPrimitive
函数。
接下来,我将一一讲解每个场景隐式转换的情况。
+ 加法运算
请原谅我要将加法运算单独作为一个小节来讲,因为 +
运算符不仅仅是数学意义上的加法,还可以是字符串的拼接操作。(JavaScript借鉴了Java中的字符串拼接语法)
关于加法运算符,读者只需要记住一点:
字符串拼接优先于数学运算。
也就是说,如果 +
两侧的值中,有一个是字符串类型,则执行字符串拼接操作,否则就是数值加法运算。请看下方例子:
'1' + 1 // '11'
1 + 1 // 2
[1] + 1 // '11'
第一例中,由于左侧有一个字符串类型的值,所以执行字符串拼接;第二例中,由于左右两侧都是数值类型,因此直接进行加法运算;第三例中,数组 [1]
会分别进行 valueOf
和 toString
操作,最后返回了字符串的 '1'
,等同于第一例,因此执行字符串拼接操作。
其他普通运算符
除了 +
之外,-
/
*
等常用运算符,都执行数值运算。看几个简单例子:
'1' - 1 // 0
[1] - 1 // 0
[1] * 1 // 1
[1] / 1 // 1
条件语句
if (test) {}
for (;test;) {}
while (test) {}
foo ? a : b
以上场景中,会对参数进行 ToBoolean
操作,而不会对参数进行隐式转换为数值或字符串类型!! 这一点千万要注意,请看这个例子:
if ([0]) {
console.log('pass') // √
} else {
console.log('unpass')
}
有些人会以为, [0]
会执行 ToPrimitive
操作得到字符串的 '0'
,然后再转换为数值的 0
,因为 0
转换为布尔值为 false
,所以应该打印 unpass
。
实则不然,条件语句只会判断当前参数是否是 Truthy !!