这是我参与8月更文挑战的第7天,活动详情查看8月更文挑战
前言
JavaScript 中的类型转换一直都是让前端开发者最头疼的问题。看网上有张图,说 JavaScript 让人不可思议。
我们来看看那张图,看输出结果是否为意料之中的结果。
因为 JavaScript 本身是一门弱类型语言,以至于类型转换发生的频率很高,本文将帮助大家梳理各种类型之间的相互转换,当然你看完整篇文章,肯定知道为什么执行上面的代码,输出那样的结果。下面,我们就开始深入理解 JavaScript 中的类型转换,让类型转换不再成为前端开发中的拦路虎。
JavaScript 类型转换分类
类型转换分为两种:显式类型转换、隐式类型转换。
显式类型转换:就是我们得动手操作,将一种值转换为另一种值
隐式类型转换:不用人为操作,程序会偷偷的进行类型转换
一般来说,类型转换主要是基本类型转基本类型、引用类型转基本类型,转换的目标类型主要分为以下几种:
- 转换为 number
- 转换为 string
- 转换为 boolean
下面会分别一一说明:
显式/强制类型转换
ToNumber:转换为 number
其他类型转换为 number 类型的规则,见下方表格
| 原始值 | 转换结果 |
|---|---|
| string | 根据语法和转换规则来转换 |
| true | 1 |
| false | 0 |
| null | 0 |
| undefined | NaN |
| symbol | TypeError: Cannot convert a Symbol value to a number |
| object | 先调用 toPrimitive,再调用 toNumber |
注:
- 根据规范,如果 Number 函数不传参数,返回 +0,如果有参数,调用
ToNumber(value)
注意这个
ToNumber表示的是一个底层规范实现上的方法,并没有直接暴露出来。
我们可以使用 + 将其他类型转为 number 类型,验证一下上面的表格的转换结果
console.log(+true); // 1
console.log(+false); // 0
console.log(+null); // 0
console.log(+undefined); // NaN
console.log(+Symbol('hi')); // Uncaught TypeError: Cannot convert a Symbol value to a number
// 前言图片的那个代码,输出的结果
console.log('b' + 'a' + + 'a' + 'a');
// "baNaNa",然后调用字符串的方法,让字符串变成小写,结果是 banana
console.log( ('b' + 'a' + + 'a' + 'a').toLowerCase() ); // "banana"
string 类型的值,转换为 number 类型的规则: 其实本质就是
- 如果字符串中只包含数字,那么就转换为对应的数字。
- 如果字符串中只包含十六进制格式,那么就转换为对应的十进制数字
- 如果字符串为空或者只包含空格,那么转换为 0
- 如果字符串包含上述之外的字符串,那么转换为 NaN。
// 第 1 点
console.log(+'123'); // 123
// 第 2 点
console.log(+'0x100f'); // 4111
// 第 3 点
console.log(+''); // 0
// 第 4 点
console.log(+'hi 234'); // NaN
ToString:转换为 string
| 原始值 | 转换结果 |
|---|---|
| number | 对应的字符串类型 |
| true | 'true' |
| false | 'false' |
| null | 'null' |
| undefined | 'undefined' |
| symbol | TypeError: Cannot convert a Symbol value to a string |
| object | 先调用 toPrimitive,再调用 toString |
我们可以使用 模板字符串 将其他类型转为 string 类型,验证一下上面的规律:
console.log(`${11}`); // "11"
console.log(`${true}`); // "true"
console.log(`${false}`); // "false"
console.log(`${null}`); // "null"
console.log(`${undefined}`); // "undefined"
console.log(`${Symbol('hi')}`); // Uncaught TypeError: Cannot convert a Symbol value to a string
console.log(`${{}}`); // "[object Object]"
ToBoolean:转换为 boolean
记住规律 5 个falsy:
在JS当中只有"0 / NaN / 空字符串'' "" / null / undefined"这个5个值转换为布尔类型的 false,其余都转换为 true 注:
- 当 Boolean 函数不传任何参数时,会返回 false
- false 本身就代表布尔值
我们可以使用 !! 将其他类型转为 boolean 类型,验证一下上面的规律:
// !! 两次取反,等价于没取反,相当于转成 boolean 类型
console.log(!!0); // false
console.log(!!NaN); // false
console.log(!!''); // false
console.log(!!null); // false
console.log(!!undefined); // false
console.log(!!Symbol()); // true
console.log(!!{}); // true
来来来,看下大家是否掌握了规律,下面代码的输出结果是?
console.log(!!false); // false
console.log(!!new Boolean(false)); // true
为什么第二个的输出结果是 true?
其实很简单,就是因为执行 new Boolean(false),相当于是创建了一个其值为false的布尔对象,所以它不是 5 个 falsy 值里面,所以结果是 true。
隐式类型转换
下面我们看看发生隐式类型转换的几个场景,以及如何转换的。
数学算符中的类型转换
减、乘、除
我们在对各种非number类型运用数学运算符(- * /)时,会先将非number类型转换为number类型;
1 - true // 0,首先把 true 转换为数字 1, 然后执行 1 - 1
1 - null // 1, 首先把 null 转换为数字 0, 然后执行 1 - 0
1 * undefined // NaN, undefined 转换为数字是 NaN
2 * ['5'] // 10, ['5']首先会变成 '5', 然后再变成数字 5
上面例子中的 ['5'] 的转换,涉及到拆箱操作。
特殊的加法
加法,比较特殊,需要做下区分。
因为 + 不仅仅是数学运算,还可以用来拼接字符串。所以需要记住下面 3 条规则:
- 当一侧为
string类型,被识别为字符串拼接,并会优先将另一侧转换为字符串类型。 - 当一侧为
number类型,另一侧为原始类型,则将原始类型转换为number类型。 - 当一侧为
number类型,另一侧为引用类型,将引用类型和number类型转换成字符串后拼接。
以上 3 点,优先级从高到低。
// 规则1
123 + '123' // "123123"
// 规则2
123 + null // 123
123 + true // 124
// 规则3
123 + {} // "123[object Object]"
if 语句和逻辑语句
在if语句和逻辑语句中,如果只有单个变量,会先将变量转换为Boolean值,5 个 falsy 值和 false 会转换成 false,其余都转换为 true
记住规律 5 个falsy:
在JS当中只有"0 / NaN / 空字符串'' "" / null / undefined"这个5个值转换为布尔类型的 false,其余都转换为 true
==
日常开发中,我们一般都是使用 === 来判断,不过了解一下 == 特点就好,因为感觉太难记了。
使用==时,若两侧类型相同,则比较结果和===相同,否则会发生隐式转换,使用==时发生的转换可以分为几种不同的情况(只考虑两侧类型不同):
情况 1
NaN和其他任何类型比较永远返回false(包括和他自己)。
NaN == NaN // false
情况 2
boolean和其他任何类型比较,boolean首先被转换为number类型。
true == 1 // true
true == '2' // false, 先把 true 变成 1,而不是把 '2' 变成 true
true == ['1'] // true, 先把 true 变成 1, ['1']拆箱成 '1', 再参考情况3
true == ['2'] // false, 同上
undefined == false // false ,首先 false 变成 0,然后参考情况4
null == false // false,同上
情况 3
string和number比较,先将string转换为number类型
123 == '123' // true, '123' 会先变成 123
'' == 0 // true, '' 会首先变成 0
情况 4
null == undefined比较结果是true,除此之外,null、undefined和其他任何结果的比较值都为false
null == undefined // true
null == '' // false
undefined == '' // false
情况 5
当原始类型和引用类型做比较时,对象类型会依照ToPrimitive规则转换为原始类型:
ToPrimitive规则,是引用类型向原始类型转变的规则,它遵循先valueOf后toString的模式期望得到一个原始类型。如果还是没法得到一个原始类型,就会抛出TypeError。
'[object Object]' == {}
// true, 对象和字符串比较,对象通过 toString 得到一个基本类型值
'1,2,3' == [1, 2, 3]
// true, 同上 [1, 2, 3]通过 toString 得到一个基本类型值
[] == ![] // true
// `!`的优先级高于`==`,`![]`首先会被转换为`false`,然后根据上面第二点,
// `false`转换成`Number`类型`0`,左侧`[]`转换为`0`,两侧比较相等。
[null] == false // true
[undefined] == false // true
// 根据数组的ToPrimitive规则,数组元素为null或undefined时,
// 该元素被当做空字符串处理,所以[null]、[undefined]都会被转换为0。
终于写完 == 了,太难用了。
下面是 x == y 的「真值表」,左边表示 x 的取值,上边表示 y 的取值,绿色表示真,白色表示假:
== 是一个难用的符号,非常非常容易出错!!
所以,我们推荐使用 === 代替 ==。
x === y 的真值表:
从 ES 规范来看类型转换
整篇文章,ToPrimitive 这个单词,出现的次数比较多。接下来,我们就来从 ES 规范来看类型转换。
先了解下 toString 和 valueOf
toString
toString() 方法返回一个表示该对象的字符串。
每一个引用类型都有
toString方法,默认情况下,toString()方法被每个Object对象继承。如果此方法在自定义对象中未被覆盖,toString()返回"[object type]",其中type是对象的类型。
值得注意的是,上面提到了如果此方法在自定义对象中未被覆盖,toString 才会达到预想的效果,事实上,大部分引用类型比如 Array、Date、RegExp 等都重写了 toString 方法。
console.log(({}).toString()) // "[object Object]"
console.log([].toString()) // ""
console.log([0].toString()) // "0"
console.log([1, 2, 3].toString()) // "1,2,3"
console.log((function(){var a = 1;}).toString()) // "function(){var a = 1;}"
console.log((/\d+/g).toString()) // "/\\d+/g"
console.log((new Date(2010, 0, 1)).toString()) // "Fri Jan 01 2010 00:00:00 GMT+0800 (中国标准时间)"
valueOf
valueOf() 方法返回指定对象的原始值。
默认的 valueOf 方法返回这个对象本身,数组、函数、正则简单的继承了这个默认方法,也会返回对象本身。日期是一个例外,它会返回它的一个内容表示: 1970 年 1 月 1 日以来的毫秒数。
var date = new Date(2021, 8, 14);
console.log(date.valueOf()); // 1631548800000
对象到字符串是如何转换的
| 参数类型 | 结果 |
|---|---|
| Object | 1. primValue = ToPrimitive(input, String) 2. 返回ToString(primValue). |
所谓的 ToPrimitive 方法,其实就是输入一个值,然后返回一个一定是基本类型的值。
小结
当我们用 String 方法转化一个值的时候
- 如果是基本类型,就"ToString:转换为 string" 对应的表格
- 如果不是基本类型,我们会调用 ToPrimitive 方法,将其转为基本类型,然后按照上面基本类型的情况,进行转换
对象到数字是如何转换的
| 参数类型 | 结果 |
|---|---|
| Object | 1. primValue = ToPrimitive(input, Number) 2. 返回ToNumber(primValue)。 |
小结
当我们用 Number 方法转化一个值的时候
- 如果是基本类型,就"ToNumber:转换为 number" 对应的表格
- 如果不是基本类型,我们会调用 ToPrimitive 方法,将其转为基本类型,然后按照上面基本类型的情况,进行转换
虽然转换成基本值都会使用 ToPrimitive 方法,但传参有不同,最后的处理也有不同,转字符串调用的是 ToString,转数字调用 ToNumber。
ToPrimitive
上面了解到对象是如何转换为字符串和数字,都会使用 ToPrimitive 方法,但传参有不同,最后的处理也有不同,转字符串调用的是 ToString,转数字调用 ToNumber。
我们先来看看 ES5 规范 9.1 函数语法表示如下:
ToPrimitive(input[, PreferredType])
第一个参数是 input,表示要处理的输入值。
第二个参数是 PreferredType,非必填,表示希望转换成的类型,有两个值可以选,Number 或者 String。
当不传入 PreferredType 时,如果 input 是日期类型,相当于传入 String,否则,都相当于传入 Number。
如果传入的 input 是 Undefined、Null、Boolean、Number、String 类型,直接返回该值。
如果是 ToPrimitive(obj, Number),处理步骤如下:
- 如果 obj 为 基本类型,直接返回
- 否则,调用 valueOf 方法,如果返回一个原始值,则 JavaScript 将其返回。
- 否则,调用 toString 方法,如果返回一个原始值,则 JavaScript 将其返回。
- 否则,JavaScript 抛出一个类型错误异常。
如果是 ToPrimitive(obj, String),处理步骤如下:
- 如果 obj为 基本类型,直接返回
- 否则,调用 toString 方法,如果返回一个原始值,则 JavaScript 将其返回。
- 否则,调用 valueOf 方法,如果返回一个原始值,则 JavaScript 将其返回。
- 否则,JavaScript 抛出一个类型错误异常。
小结
引用类型转换为 number 类型
其实就是Number 函数背后的转换规则
第一步,调用对象自身的valueOf方法。如果返回原始类型的值,则直接对该值使用Number函数,不再进行后续步骤。
第二步,如果valueOf方法返回的还是对象,则改为调用对象自身的toString方法。如果toString方法返回原始类型的值,则对该值使用Number函数,不再进行后续步骤。
第三步,如果toString方法返回的是对象,就报错。
引用类型转换为 string 类型
其实就是 String 函数背后的转换规则,与Number方法基本相同,只是互换了valueOf方法和toString方法的执行顺序。
第一步,先调用对象自身的toString方法。如果返回原始类型的值,则对该值使用String函数,不再进行以下步骤。
第二步,如果toString方法返回的是对象,再调用原对象的valueOf方法。如果valueOf方法返回原始类型的值,则对该值使用String函数,不再进行以下步骤。
第三步,如果valueOf方法返回的是对象,就报错。
结束了吗?还没呢,还可以继续往下深入了解,大家可以根据实际情况,决定是否继续往下哈。
在对象转原始类型的时候,一般会调用内置的 ToPrimitive 方法,而 ToPrimitive 方法则会调用 OrdinaryToPrimitive 方法,我们可以看一下 ECMA 的官方文档
大佬的翻译
ToPrimitive 方法接受两个参数,一个是输入的值 input,一个是期望转换的类型 PreferredType。
- 如果没有传入
PreferredType参数,让hint等于"default" - 如果
PreferredType是hint String,让hint等于"string" - 如果
PreferredType是hint Number,让hint等于"number" - 让
exoticToPrim等于GetMethod(input, @@toPrimitive),意思就是获取参数input的@@toPrimitive方法 - 如果
exoticToPrim不是Undefined,那么就让result等于Call(exoticToPrim, input, « hint »),意思就是执行exoticToPrim(hint),如果执行后的结果result是原始数据类型,返回result,否则就抛出类型错误的异常 - 如果
hint是"default",让hint等于"number" - 返回
OrdinaryToPrimitive(input, hint)抽象操作的结果
大白话来理解的话,在进行类型转换的时候,一般是通过 toPrimitive 方法,将引用类型转换为基本类型。如果引用类型上有 @@toPrimitive 方法,就调用 @@toPrimitive 方法,相当于执行上面说的 exoticToPrim,执行的返回值为原始值,则直接返回,否则,就抛出错误异常。
OrdinaryToPrimitive
而 OrdinaryToPrimitive 方法也接受两个参数,一个是输入的值 O,一个也是期望转换的类型 hint。
- 如果输入的值是个对象
- 如果
hint是个字符串并且值为'string'或者'number' - 如果
hint是'string',那么就将methodNames设置为toString、valueOf - 如果
hint是'number',那么就将methodNames设置为valueOf、toString - 遍历
methodNames拿到当前循环中的值name,将method设置为O[name](即拿到valueOf和toString两个方法) - 如果
method可以被调用,那么就让result等于method执行后的结果,如果result不是对象就返回result,否则就抛出一个类型错误的报错。
大白话来理解的话,调用 OrdinaryToPrimitive 方法的时候,需要判断期望转换的目标类型,决定是先调用 toString 还是 valueOf 方法,如果执行这两个方法后得到了原始类型的值,那么就返回,否则,就抛出错误异常。
Symbol.toPrimitive
在 ES6 之后提供了 Symbol.toPrimitive 方法,该方法在类型转换的时候优先级最高。
const obj = {
toString() {
return '1111'
},
valueOf() {
return 222
},
[Symbol.toPrimitive]() {
return 666
}
}
const num = 1 + obj; // 667
const str = '1' + obj; // '1666'
参考
- 数据类型的转换
- 帮你彻底弄懂 JavaScript 类型转换
- 数据类型转换看这篇就够了
- 【JS 进阶】你真的掌握变量和类型了吗
- JavaScript 深入之头疼的类型转换(上)
- 搞懂 JavaScript 类型转换
文中如有错误,欢迎在评论区指正,如果这篇文章帮助到了你或者喜欢,欢迎点赞和关注。