likes
comments
collection
share

一道面试题带你认透js类型转换(万字解析?)

作者站长头像
站长
· 阅读数 9

笔者很想让大家搞懂js类型转换问题,如果你觉得很简单,你不妨看看下面的这道题

面试官:下面输出结果是什么,并解释其原因

console.log([] == ![]) 

答案我放在文章末尾了,小伙伴们也可以先去对下答案,为了给大家弄懂答案的意思,我还是老套路,先给大家过一遍类型转换,套路很多!


类型转换就是不同的数据类型之间进行转换,数据类型我们就算es6之前也就只有6种,5种原始,1个对象(引用就统称为对象了),而undefinednull我们不会来转它啊,因此,JavaScript种就只有三种类型的转换:

  • 转数字
  • 转字符
  • 转布尔

接下来我们就分别对原始类型和对象进行讲解,原始其实很简单,一张图就能搞定。对象会比较搞心态,不过看完本文,相信你能轻松解决文章开头的面试题

原始

原始类型我们这里只讨论五种,数字,字符,布尔,undefined,null

这里面三个转换一定要熟悉,否则对象的三个转换和运算符转换你可能会蒙圈

转数字

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中的一员

console.log(NaN == NaN) // false

这里注意,两个NaN永远都不可能相等

转字符

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

原始转字符串是最简单粗暴的,全给你转成了字符串!

转布尔

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

一张图其实就可以搞定原始之间的类型转换

一道面试题带你认透js类型转换(万字解析?)

原始转对象

console.log(Object('hello')) // String {'hello'}  字符串对象
console.log(Object(123)) // Number {123}  数字对象
console.log(Object(true)) // Boolean {true} 布尔对象

这里其实就是给这三个原始类型实例化包装类对象

对象

同样的,对象只会转下面这三个原始类型,不会去转undefined和null,对象转原始会比较磨人

转数字

我们先看下valueOf这个方法

valueOf

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

一道面试题带你认透js类型转换(万字解析?)

安利一个谷歌翻译插件👍:沉浸式翻译

表格中,原始转数字我们在上面已经讲完了,现在就是看对象转数字:让对象去调用ToPrimitive(input argument, hint Number),点进 ToPrimitive,我们可以看到它的原理

一道面试题带你认透js类型转换(万字解析?)

再点进 8.12.8看看规则

这里有个分支,如果hint暗示是数字就走下面的规则

一道面试题带你认透js类型转换(万字解析?)

总结下这个内容

ToPrimitive(obj, Number)

  1. 如果是基本类型,不进行转换
  2. 否则,调用valueOf方法,如果得到原始值,则返回
  3. 否则,调用toString方法,如果得到原始值,则返回
  4. 否则,报错

注意:像是ToPrimitive这样首字母也大写的方法是给V8引擎去看的,我们用不了这种官方方法,但是我们需要理解它的过程

好,我们现在来看看下面的栗子

对象转数字🌰

console.log(Number({})) // NaN 

对象不是原始类型,转到第二步,valueOf这个方法是服务于包装类对象的,因此去到第三步,对象转字符串。

对象调用toString()方法不就是调用这个Object.prototype.toString()嘛,给你判断数据类型用的,这里this指向的就是对象,因此判断为Object,输出为[object Object]字符串,所以toPrimitive将对象转化成了一个[object Object]字符串,然后再去调用ToNumber也就是转数字,一个非数字字符串转数字上面原始转数字已经讲过了,就是NaN

数组转数字🌰

有了上面的规则,我们现在再来看下数组

console.log(Number([])) // 0

空数组非原始,走第二步,空数组调用不了valueOf方法,因此去到第三步,去调用toString方法,也就是数组转字符串。

之前我们讲过,toString很牛,啥东西都给你转成字符串,这里也一样,空数组转字符串就是空字符串,因此toPrimitive返回了一个空字符串,再去调用ToNumber方法,空字符串转数字上面也已经讲了,就是0!

转字符

同样,我们看下官方文档如何解释转字符串

依旧是这个链接:es5.github.io/#x9.8

一道面试题带你认透js类型转换(万字解析?)

同样的,原始转字符串上面已经讲过了,直接看对象这块儿,同样调用ToPrimitive方法,不过hint暗示的值是不同的,这里暗示的是字符串,我们依旧点进 8.12.8查看对应的规则

一道面试题带你认透js类型转换(万字解析?)

总结下这个内容,其实就是将hint Number步骤二,三反了一下,这里是先toStringvalueOf

ToPrimitive(obj, String)

  1. 如果是基本类型,不进行转换
  2. 否则,调用toString方法,如果得到原始值,则返回
  3. 否则,调用valueOf方法,如果得到原始值,则返回
  4. 否则,报错

对象转字符串🌰

console.log(({}).toString()) // 字符串[object Object]

这里写成{}.toString()会报错,因为会识别成{ }.toString(),因此我们用个括号括起来

这个输出结果现在已经很好得出了,转字符时碰到是对象,去ToPrimitive,如果是暗示字符,就是走上面那四个步骤,对象非原始,第二部,去调用toString,对象调用toString就是判断此时this的数据类型,因此是对象,输出[object Object]字符串,toPrimitive返回了这个字符串,再去ToString,已经是字符串了就不需要去转换,因此输出结果就是字符串[object Object]

数组转字符串🌰

console.log(([]).toString()) // 空字符串

还是按照步骤来,第一步走不通,去第二步调用toString,空数组转字符串就是空字符串,ToPrimitive返回空字符串,再调用ToString,于是还是输出空字符串

我们给数组放点东西

console.log(([1, 2, 3]).toString()) // 

第一步走不通,于是去调用第二步,调用toString,还是一样的,toString很强大,啥都给你转成字符串,因此这里就返回一个字符串1, 2, 3,因此就是输出字符串1, 2, 3,你放非数字也是一样的

同样的,如果你放日期进去,还是以一个字符串的形式输出,函数同理

打消你的疑惑

你是否发现,ToPrimitive好像永远走不到第三步,因为对象都会有toString方法,其实这是为了防止toString被重写

转布尔

console.log(Boolean({})) // true
console.log(Boolean([])) // true
console.log(Boolean([1, 2, 3])) // true

对象转布尔规则很简单:永远为true,无论对象空与否


再来看看运算符里面的隐式转换

一元运算符 +

常见的一元运算符有delete、?、!、+ 这里只有+需要讲解

看下官方文档的讲解

一道面试题带你认透js类型转换(万字解析?)

这个步骤很简单,就是统一让参数去调用ToNumber

举个栗子🌰

console.log(+ '1') // 数字1

这个就是让数字字符串转换成数字,规则就是原始类型中的

所以下面同理,不清楚再看下上面的原始转数字

console.log(+ 'a') // NaN
console.log(+ '') // 0

再来个栗子🌰

console.log(+ {}) // NaN

就是让对象调用ToNumber,对象转数字,走hint暗示数字的ToPrimitive规则,去了第三步toString,得到字符串[object Object],然后再ToNumber,也就是字符串转数字,又到原始类型了,因此原始类型的三个转换一定要牢记于心,非空非数字字符串转数字就是NaN

再来个栗子🌰

console.log(+ []) // 0

同样的,数组调用ToNumber,走规则为hint数字的ToPrimitive,到了步骤三,调用toString,空数组转字符串就是空字符串,然后返回结果的空字符串再去ToNumber转数字,就是0

再来个栗子🌰

console.log(+ [1, 2, 3]) // NaN

同上,数组调用ToNumber,走规则为hint数字的ToPrimitive,步骤三,返回一个字符串1, 2, 3,字符串再去转数字,只要字符串是非空的,转数字一定是0

再来个栗子🌰看看你会了没

console.log(+ [123]) // 数字123

同上,最终得到数字字符串123,然后再去转数字123

二元运算符 +

同样的,我们看看官方文档如何介绍 二元+,定位到11.6.1

一道面试题带你认透js类型转换(万字解析?)

总结下这里的内容

  1. 如果左右两个对象中有一个是字符串,那么两个都调用ToString再进行拼接
  2. 否则,两个都调用ToNumber,调用过程中如果两个结果中有一个字符串,则再走步骤一,否则数字相加

举个栗子🌰

console.log(1 + 1) // 2

这要你解释?hhh,虽然都知道这个道理,其实就是走了步骤二,两个参数都不是字符串,所以调用ToNumber

再来个栗子🌰

console.log(1 + '1') // 字符串11

走步骤一,两个参数中有一个就会导致两个都去ToString转字符串然后再进行拼接

再来个栗子🌰

console.log(1 + null) // 1

null转数字就是0,还是文章最前面的原始规则,因此相加为1

再来个栗子🌰

console.log(1 + undefined) // NaN

undefined转数字就是NaN,NaN加一个原始类型还是NaN

再来个栗子🌰

console.log([] + []) // 空字符串

这里其实还是很坑的,我最开始以为这里会输出0,因为两个空字符串转数字都是0嘛,然后0 + 0

其实二元+只要是碰到了字符串,这个+就是起到一个拼接的作用,也就是说这里两个空数组调用ToNumber会去调用hint数字的ToPrimitive走到第三步调用toString,得到空字符了就会进行字符串拼接,也就是步骤二的调用过程中出现了字符串就去步骤一,因此你可以看出二元+对字符串非常的敏感

再来个栗子🌰

console.log([] + {}) // [object Object]

数组和对象都要进行ToNumber操作,[]会走到一个空字符串,然后{}会走到字符串[object Object],然后两者进行拼接

再来个栗子🌰

console.log({} + {}) // [object Object][object Object]

没有问题,两个都是字符串[object Object],然后进行拼接

小坑

在面试中,这里也有个坑,面试官会问你下面会输出什么(这里放在浏览器中运行)

{} + {}

啊,这不就是两个字符串[object Object]进行拼接嘛,其实这里输出的是NaN,说坑主要是一个写法问题,{} + {}这样写在浏览器看来是{} +{},先进行一元+,也就是得到了一个{} NaN最后输出一个NaN,这里不用管前面的对象。如果你不用console.log就要写成({}) + ({})才行。

二元运算符 ==

此前我们讲过==就是比较数据类型和数值,返回一个布尔值

今天我们再来深入一下,官方文档定位到11.9.3

这个规则太多了,我这里就只截取一张放这里

一道面试题带你认透js类型转换(万字解析?)

来个栗子🌰

console.log(1 == '1') // true

文档给出解释

一道面试题带你认透js类型转换(万字解析?)

如果二者是数字和字符串组合的,字符串会转换成数字然后再进行比较

再来个栗子🌰

console.log(NaN == NaN) // false

这里官方文档说的是,只要参数有一个NaN,那么一定是false

再来个栗子🌰

console.log(1 == {}) // 

文档给到你

一道面试题带你认透js类型转换(万字解析?)

只要是对象和字符串或数字的组合,一定是对对象进行ToPrimitive,这里是数字,就是hint为数字版的ToPrimitive,因此对象会被转换成字符串[object Object],这个过程已经讲了太多遍了,这里就不再赘述了,相信大家也已经明白了。所以就是字符串比较数字了,所以现在变成了字符串数字组合,需要将字符串转数字,非数字非空字符串转数字就是NaN出现NaN一定是false

再来个栗子🌰

console.log(false == []) // true

一道面试题带你认透js类型转换(万字解析?)

因此这个需要将这个布尔值转换成数字,false转数字就是0,然后就是空数组了,空数组转数字先经历到空字符,然后再是0,因此这个就是输出true

其实就这几个栗子你也能发现,==的隐式转换也是最终到number


答案

console.log([] == ![]) // true

这里有两个运算符,!优先级高于==,因此先执行![]!的执行步骤是将该对象转换成布尔类型,然后进行去非,[]转换成布尔,对象转布尔规则中有说道,任何引用类型转布尔都是true,因此![]就是false,然后再来判断[] == false,==存在隐式类型转换,最终都会转换成数字,false转数字就是0;[]转换成Number对应的ToPrimitive调用valueOf行不通(valueOf用于转换包装类才有效),于是去调用toString,空数组转字符串得到空字符,空字符转数字就是0,所以最终式子为0 == 0,返回true

参考资料: JavaScript官方文档:es5.github.io/#x9.8

据说如果你能够把语言的官方文档给全部看懂,你就可以自信的在简历上写下精通JavaScript了

一道面试题带你认透js类型转换(万字解析?)