深入类型转换
前言
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 || ba && 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) // 2const 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
该函数用于将任意值转换为字符串类型的值,它的规则如下所示:
- 如果传入参数为一个字符串,返回这个参数
- 如果参数为
undefinednulltruefalse,分别返回'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 !!