非常不好意思,其实本文没有万字哈,但是笔者我真的很想让大家搞懂js类型转换问题,如果你觉得很简单,你不妨看看下面的这道题
面试官:下面输出结果是什么,并解释其原因
css
console.log([] == ![])
答案我放在文章末尾了,为了给大家弄懂答案的意思,我还是老套路,先给大家过一遍类型转换
类型转换就是不同的数据类型之间进行转换,数据类型我们就算es6之前也就只有6种,5种原始,1个对象(引用就统称为对象了),而undefined
和null
我们不会来转它啊,因此,JavaScript种就只有三种类型的转换:
- 转数字
- 转字符
- 转布尔
接下来我们就分别对原始类型和对象进行讲解,原始其实很简单,一张图就能搞定。对象会比较搞心态,不过看完本文,相信你能轻松解决文章开头的面试题
原始
原始类型我们这里只讨论五种,数字,字符,布尔,undefined,null
这里面三个转换一定要熟悉,否则对象的三个转换和运算符转换你可能会蒙圈
转数字
javascript
console.log(+"123") // 数字123 + 最终都是朝着数字进行转换,稍后再讲
console.log(Number()) // 0 默认为0
console.log(Number(123)) // 数字123
console.log(Number("123")) // 数字123 字符串中如果是数字,转数字就会成功
console.log(Number("abc")) // NaN 这东西是个值,是number中一个特殊值,代表无法表示的值
console.log(Number('')) // 0 没有东西就是0
consolo.log(Number('123abc')) // NaN 123abc还是个字符串
console.log(Number('100.123')) // 数字100.123
console.log(Number('00100.123')) // 数字100.123
console.log(Number(true)) // 1
console.log(Number(false)) // 0 这样理解:1真,0假
console.log(Number(undefined)) // NaN
console.log(Number(null)) // 0
这里讲一下NaN
NaN
全称
not a number
中文名"非数字",和Infinity类似,都是特殊数字值,注意,他们都是number中的一员
ini
console.log(NaN == NaN) // false
这里注意,两个NaN
永远都不可能相等
转字符
javascript
console.log(String()) // 空字符串
console.log(String(123)) // 字符串123 在node中字符串是黑色的
console.log(String(NaN)) // 字符串NaN
console.log(String(Infinity)) // 字符串Infinity
console.log(String(true)) // 字符串true ,false同理
console.log(String(undefined)) // 字符串undefined
console.log(String(null)) // 字符串null
原始转字符串是最简单粗暴的,全给你转成了字符串!
转布尔
sql
console.log(Boolean()) // false 默认就是false
console.log(Boolean(true)) // true
console.log(Boolean(0)) // false 数字中,除了0其余都是true,哪怕是负值也是true
console.log(Boolean('123')) // true 字符串只要是有值,都是true
console.log(Boolean(' ')) // true 空格也是有对应的ASCII码值的,因此也算是有值
console.log(Boolean('')) // false 空字符串没有值就是false
console.log(Boolean(undefined)) // false
console.log(Boolean(null)) // false
原始类型转布尔最需要注意的地方就是字符串,字符串只要有内容,哪怕空格,都是true。
一张图其实就可以搞定原始之间的类型转换
原始转对象
javascript
console.log(Object('hello')) // String {'hello'} 字符串对象
console.log(Object(123)) // Number {123} 数字对象
console.log(Object(true)) // Boolean {true} 布尔对象
这里其实就是给这三个原始类型实例化包装类对象
对象
同样的,对象只会转下面这三个原始类型,不会去转undefined和null,对象转原始会比较磨人
转数字
我们先看下valueOf
这个方法
valueOf
javascript
let num = new Number(123)
console.log(num.valueOf()) // 数字123
let str = new String('hello')
console.log(str.valueOf()) // 字符串hello
let boo = new Boolean(true)
console.log(boo.valueOf()) // true
let obj = {}
console.log(obj.valueOf()) // {} 没有变化
let arr = []
console.log(arr.valueOf()) // [] 没有变化
因此,我们可以得出结论,valueOf
是用于将包装类对象转成原始类型,改变不了常规对象
我们看下官方文档如何解释转数字
链接给到你:es5.github.io/#x9.8
安利一个谷歌翻译插件👍:沉浸式翻译
表格中,原始转数字我们在上面已经讲完了,现在就是看对象转数字:让对象去调用ToPrimitive(input argument, hint Number)
,点进 ToPrimitive,我们可以看到它的原理
再点进 8.12.8看看规则
这里有个分支,如果hint暗示是数字就走下面的规则
总结下这个内容
ToPrimitive(obj, Number)
- 如果是基本类型,不进行转换
- 否则,调用
valueOf
方法,如果得到原始值,则返回 - 否则,调用
toString
方法,如果得到原始值,则返回 - 否则,报错
注意:像是
ToPrimitive
这样首字母也大写的方法是给V8引擎去看的,我们用不了这种官方方法,但是我们需要理解它的过程
好,我们现在来看看下面的栗子
对象转数字🌰
javascript
console.log(Number({})) // NaN
对象不是原始类型,转到第二步,valueOf
这个方法是服务于包装类对象的,因此去到第三步,对象转字符串。
对象调用toString()
方法不就是调用这个Object.prototype.toString()
嘛,给你判断数据类型用的,这里this指向的就是对象,因此判断为Object
,输出为[object Object]
字符串,所以toPrimitive
将对象转化成了一个[object Object]
字符串,然后再去调用ToNumber
也就是转数字,一个非数字字符串转数字上面原始转数字已经讲过了,就是NaN
!
数组转数字🌰
有了上面的规则,我们现在再来看下数组
javascript
console.log(Number([])) // 0
空数组非原始,走第二步,空数组调用不了valueOf
方法,因此去到第三步,去调用toString
方法,也就是数组转字符串。
之前我们讲过,toString
很牛,啥东西都给你转成字符串,这里也一样,空数组转字符串就是空字符串 ,因此toPrimitive
返回了一个空字符串,再去调用ToNumber
方法,空字符串转数字上面也已经讲了,就是0!
转字符
同样,我们看下官方文档如何解释转字符串
依旧是这个链接:es5.github.io/#x9.8
同样的,原始转字符串上面已经讲过了,直接看对象这块儿,同样调用ToPrimitive
方法,不过hint暗示的值是不同的,这里暗示的是字符串,我们依旧点进 8.12.8查看对应的规则
总结下这个内容,其实就是将hint Number步骤二,三反了一下,这里是先toString
后valueOf
ToPrimitive(obj, String)
- 如果是基本类型,不进行转换
- 否则,调用
toString
方法,如果得到原始值,则返回 - 否则,调用
valueOf
方法,如果得到原始值,则返回 - 否则,报错
对象转字符串🌰
scss
console.log(({}).toString()) // 字符串[object Object]
这里写成
{}.toString()
会报错,因为会识别成{ }.toString()
,因此我们用个括号括起来
这个输出结果现在已经很好得出了,转字符时碰到是对象,去ToPrimitive
,如果是暗示字符,就是走上面那四个步骤,对象非原始,第二部,去调用toString
,对象调用toString
就是判断此时this的数据类型,因此是对象,输出[object Object]
字符串,toPrimitive
返回了这个字符串,再去ToString
,已经是字符串了就不需要去转换,因此输出结果就是字符串[object Object]
数组转字符串🌰
scss
console.log(([]).toString()) // 空字符串
还是按照步骤来,第一步走不通,去第二步调用toString
,空数组转字符串就是空字符串,ToPrimitive
返回空字符串,再调用ToString
,于是还是输出空字符串
我们给数组放点东西
scss
console.log(([1, 2, 3]).toString()) //
第一步走不通,于是去调用第二步,调用toString
,还是一样的,toString
很强大,啥都给你转成字符串,因此这里就返回一个字符串1, 2, 3
,因此就是输出字符串1, 2, 3
,你放非数字也是一样的
同样的,如果你放日期进去,还是以一个字符串的形式输出,函数同理
打消你的疑惑
你是否发现,ToPrimitive
好像永远走不到第三步,因为对象都会有toString
方法,其实这是为了防止toString
被重写
转布尔
javascript
console.log(Boolean({})) // true
console.log(Boolean([])) // true
console.log(Boolean([1, 2, 3])) // true
对象转布尔规则很简单:永远为true,无论对象空与否
再来看看运算符里面的隐式转换
一元运算符 +
常见的一元运算符有delete、?、!、+
这里只有+
需要讲解
看下官方文档的讲解
这个步骤很简单,就是统一让参数去调用ToNumber
举个栗子🌰
arduino
console.log(+ '1') // 数字1
这个就是让数字字符串转换成数字,规则就是原始类型中的
所以下面同理,不清楚再看下上面的原始转数字
javascript
console.log(+ 'a') // NaN
console.log(+ '') // 0
再来个栗子🌰
arduino
console.log(+ {}) // NaN
就是让对象调用ToNumber
,对象转数字,走hint暗示数字的ToPrimitive
规则,去了第三步toString
,得到字符串[object Object]
,然后再ToNumber
,也就是字符串转数字,又到原始类型了,因此原始类型的三个转换一定要牢记于心,非空非数字字符串转数字就是NaN
再来个栗子🌰
arduino
console.log(+ []) // 0
同样的,数组调用ToNumber
,走规则为hint数字的ToPrimitive
,到了步骤三,调用toString
,空数组转字符串就是空字符串,然后返回结果的空字符串再去ToNumber
转数字,就是0
再来个栗子🌰
arduino
console.log(+ [1, 2, 3]) // NaN
同上,数组调用ToNumber
,走规则为hint数字的ToPrimitive
,步骤三,返回一个字符串1, 2, 3
,字符串再去转数字,只要字符串是非空的,转数字一定是0
再来个栗子🌰看看你会了没
arduino
console.log(+ [123]) // 数字123
同上,最终得到数字字符串123,然后再去转数字123
二元运算符 +
同样的,我们看看官方文档如何介绍 二元+
,定位到11.6.1
总结下这里的内容
- 如果左右两个对象中有一个是字符串,那么两个都调用
ToString
再进行拼接 - 否则,两个都调用
ToNumber
,调用过程中如果两个结果中有一个字符串,则再走步骤一,否则数字相加
举个栗子🌰
arduino
console.log(1 + 1) // 2
这要你解释?hhh,虽然都知道这个道理,其实就是走了步骤二,两个参数都不是字符串,所以调用ToNumber
再来个栗子🌰
arduino
console.log(1 + '1') // 字符串11
走步骤一,两个参数中有一个就会导致两个都去ToString转字符串然后再进行拼接
再来个栗子🌰
javascript
console.log(1 + null) // 1
null转数字就是0,还是文章最前面的原始规则,因此相加为1
再来个栗子🌰
javascript
console.log(1 + undefined) // NaN
undefined转数字就是NaN,NaN加一个原始类型还是NaN
再来个栗子🌰
arduino
console.log([] + []) // 空字符串
这里其实还是很坑的,我最开始以为这里会输出0,因为两个空字符串转数字都是0嘛,然后
0 + 0
其实二元+只要是碰到了字符串,这个+就是起到一个拼接的作用,也就是说这里两个空数组调用ToNumber
会去调用hint数字的ToPrimitive
走到第三步调用toString
,得到空字符了就会进行字符串拼接,也就是步骤二的调用过程中出现了字符串就去步骤一,因此你可以看出二元+对字符串非常的敏感
再来个栗子🌰
arduino
console.log([] + {}) // [object Object]
数组和对象都要进行ToNumber
操作,[]会走到一个空字符串,然后{}会走到字符串[object Object]
,然后两者进行拼接
再来个栗子🌰
vbnet
console.log({} + {}) // [object Object][object Object]
没有问题,两个都是字符串[object Object]
,然后进行拼接
小坑
在面试中,这里也有个坑,面试官会问你下面会输出什么(这里放在浏览器中运行)
{} + {}
啊,这不就是两个字符串[object Object]
进行拼接嘛,其实这里输出的是NaN
,说坑主要是一个写法问题,{} + {}
这样写在浏览器看来是{} +{}
,先进行一元+,也就是得到了一个{} NaN
最后输出一个NaN
,这里不用管前面的对象。如果你不用console.log
就要写成({}) + ({})
才行。
二元运算符 ==
此前我们讲过==就是比较数据类型和数值,返回一个布尔值
今天我们再来深入一下,官方文档定位到11.9.3
这个规则太多了,我这里就只截取一张放这里
来个栗子🌰
ini
console.log(1 == '1') // true
文档给出解释
如果二者是数字和字符串组合的,字符串会转换成数字然后再进行比较
再来个栗子🌰
ini
console.log(NaN == NaN) // false
这里官方文档说的是,只要参数有一个NaN,那么一定是false
再来个栗子🌰
arduino
console.log(1 == {}) //
文档给到你
只要是对象和字符串或数字的组合,一定是对对象进行ToPrimitive
,这里是数字,就是hint为数字版的ToPrimitive
,因此对象会被转换成字符串[object Object],这个过程已经讲了太多遍了,这里就不再赘述了,相信大家也已经明白了。所以就是字符串比较数字了,所以现在变成了字符串数字组合,需要将字符串转数字,非数字非空字符串转数字就是NaN
,出现NaN一定是false
再来个栗子🌰
arduino
console.log(false == []) // true
因此这个需要将这个布尔值转换成数字,false转数字就是0,然后就是空数组了,空数组转数字先经历到空字符,然后再是0,因此这个就是输出true
其实就这几个栗子你也能发现,==的隐式转换也是最终到number
答案
arduino
console.log([] == ![]) // true
这里有两个运算符,
!
优先级高于==
,因此先执行![]
。!
的执行步骤是将该对象转换成布尔类型,然后进行去非,[]
转换成布尔,对象转布尔规则中有说道,任何引用类型转布尔都是true
,因此![]
就是false
,然后再来判断[] == false
,==存在隐式类型转换,最终都会转换成数字,false
转数字就是0;[]
转换成Number对应的ToPrimitive
调用valueOf
行不通(valueOf
用于转换包装类才有效),于是去调用toString
,空数组转字符串得到空字符,空字符转数字就是0,所以最终式子为0 == 0
,返回true
参考资料: JavaScript官方文档:es5.github.io/#x9.8
据说如果你能够把语言的官方文档给全部看懂,你就可以自信的在简历上写下精通JavaScript了
如果觉得本文对你有帮助的话,可以给个免费的赞吗[doge] 还有可以给我的gitee链接codeSpace: 记录coding中的点点滴滴 (gitee.com)点一个免费的star吗[星星眼]