前言
想必大家在面试当中,往往都遇到过类似于给你一段奇怪的代码[] == ![]
让你写出输出结果的场景,本次就帮你一次搞懂js隐式转换,面试不再踩坑
分析:为什么有隐式转换?
想知道什么是隐式转换,首先我们先搞懂什么是显示类型转换
?
在执行某些操作时,由于我们需要调用对应类型所提供的api,或进行运算,会主动的改变变量的类型,例如String(),Number(),parsenInt(),toString()..等类似的方法,此类由我们手动转换类型的方式,称为显示类型转换
那么隐式类型转换
呢?
JavaScript是弱类型语言,这意味着它不像Java,C++一样的强类型语言有预先确定的类型。
并且变量的类型是由值的类型来决定的,这导致了一个问题,一个变量可能上一步骤操作中还是String,下一步操作可能立刻变为了Object,为了解决不同类型无法进行计算,JS底层会将不同类型转换为同一类型,由JS运行环境自动帮我们去做的类型转换,称为隐式类型转换
JS数据类型
JS的两大类数据类型:
原始类型:Undefined、 Null、 String、 Number、 Boolean、Symbol
引用类型:Object
Symbol是由ES6新提出的,我们这里先不讲
转换规则
ECMAScript 运行时系统会在需要时从事自动类型转换。为了阐明某些结构的语义,定义一集转换运算符是很有用的。这些运算符不是语言的一部分;在这里定义它们是为了协助语言语义的规范。转换运算符是多态的 — 它们可以接受任何 ECMAScript 语言类型 的值,但是不接受 规范类型 。
既然规则是ES5规范(见第9章类型转换)定义的,那就没必要讨论为什么了,记住怎么用就好了。
我们来了解三个概念:
- 转换为原始值
- 转换为数字
- 转换为字符串 因为上边文档当中已有比较详细的描述,这里只做些简单的解释,详情请查阅文档
ToPrimitive(转换为原始值)
/**
* @obj 需要转换的对象
* @type 期望转换为的原始数据类型,可选
*/
ToPrimitive(obj,type) //该方法接收两个参数
type可以为number或者string,两者的执行顺序有一些差别
string:调用Object的toString方法,如果为原始值则返回,否则再去调用Object的valueOf方法,如果为原始值则返回,否则抛出TypeError异常
number:调用Object的valueOf方法,如果为原始值则返回,否则再去调用Object的toString方法,如果为原始值则返回,否则抛出TypeError异常
其实就是调用方法先后,毕竟期望数据类型不同,如果是string当然优先调用toString。反之亦然。
并且type参数可以为空,这时候type的默认值会按照下面的规则设置:
该对象为Date,则type被设置为String否则,type被设置为Number
对于Date数据类型,我们更多期望获得的是其转为时间后的字符串,而非毫秒值,如果为number,则会取到对应的毫秒值,显然字符串使用更多。
其他类型对象按照取值的类型操作即可。
概括而言,ToPrimitive转成何种原始类型,取决于type,type参数可选,若指定,则按照指定类型转换,若不指定,默认根据实用情况分两种情况,Date为string,其余对象为number。那么什么时候会指定type类型呢,那就要看下面两种转换方式了。
toNumber
某些特定情况下需要用到ToNumber方法来转成number
如果为原始类型则转换为该参数的数值类型:
输入类型 | 结果 |
---|---|
Undefined | NaN |
Null | +0 |
Boolean | 如果参数是true,结果为1 如果参数是false,此结果为+0 |
Number | 结果等于输入的参数(不转换) |
String | 情况比较多,参考规范 |
Object | 依次调用 1.先获取原始值调用ToPrimitive(输入参数type为number) 2.再将原始类型调用ToNumber(原始值) |
ps:对于String类型,特殊的情况比较多,一般只要掌握常见的就可以,和直接调用Number()函数结果是一致的。
需要注意的是,如果当参数为Object类型的时候,会先调用ToPrimitive,type指定为number类型,获取原始类型后再调用ToNumber
toString
ToString 运算符根据下表将其参数转换为字符串类型的值:
输入类型 | 结果 |
---|---|
Undefined | "undefined" |
Null | "null" |
Boolean | 如果参数是true,结果为"true" 如果参数是false,此结果为"false" |
Number | 情况比较多,参考规范 |
String | 结果等于输入的参数(不转换 |
Object | 依次调用 1.先获取原始值调用ToPrimitive(输入参数type为string) 2.再将原始类型调用ToString(原始值) |
常见的规律
- Undefined,null,boolean直接加上引号,例如'null'
- number的规范则比较多,1为"1",-1为"-1",NaN为"NaN"
- 对象则是先转为原始值,再按照上面的步骤进行处理。
valueOf
当调用valueOf方法时,步骤如下:
- 调用ToObject方法得到一个对象
(其实就是包装类型,感兴趣的同学可以自己去了解下)
- 原始数据类型转换为对应的内置对象,引用类型(Object)则不变
- 调用该对象内置的valueOf方法(继承自Object.prototype对象) 不同内置对象的valueOf实现:
- String => 返回字符串值
- Number => 返回数字值
- Date => 返回一个数字,即时间值,字符串中内容是依赖于具体实现的
- Boolean => 返回Boolean的this值
- Object => 返回this
隐式转换:重头戏来了 []==![]
前面讲了那么多概念,其实就是为了大家可以充分理解,在运算隐式转换时,js究竟都做了什么
例:+运算
- 在进行运算时+左右分别进行进行ToPrimitive()操作,获取原始值
- 如果获取的原始值当中包含String,则对所有原始值执行toString处理后进行拼接
- 其他的都进行toNumber处理
- 在转换时ToPrimitive,除去Date为String外都按照ToPrimitive type为Number进行处理
1+'2'+false // '12false'
// 我们来拆解一下运算过程:
// 按执行顺序从左到右执行,先计算1+'2'
// 1. 左右两边同事进行ToPrimitive()操作,左边为原始类型,依旧是Number,右边为String
// 2. 因为返回的原始值当中包含String,于是对所有原始值进行toString处理,变为 '1'+'2',得到结果'12'
// 3. 然后重复第一步操作,计算'12'+false
// 4. 左右两边都为原始值,但是'12'为String类型,则布尔值也转为'false'
// 5. '12'+'false' 进行拼接得到最后结果 '12false'
例:引用类型进行计算
var obj1 = {
valueOf:function(){
return 1
}
}
var obj2 = {
toString:function(){
return 'a'
}
}
// 下面我们还是拆解一下运算过程
1+obj1 // 2
// 1. 左右两边同时进行ToPrimitive()操作,左边为原始类型,依旧是Number,右边为引用类型,按照type为number进行转换
// 2. 先调用obj1.valueOf方法,我们知道引用类型不会进行包装,于是直接调用obj1内部的valueOf方法,返回1
// 3. 得到两边都是number类型,于是直接进行相加1 + 1,输出2
1+obj2 // 1a
// 1. 左右两边同时进行ToPrimitive()操作,左边为原始类型,依旧是Number,右边为引用类型,按照type为number进行转换
// 2. 先调用obj2.valueOf方法,我们知道引用类型不会进行包装,于是直接调用obj2内部的valueOf方法,因为valueOf方法没有重写,于是调用的是Object.prototype.valueOf返回的是obj2的this,发现得到的不是一个原始值,于是继续调用toString方法,返回 'a'
// 3. 得到2个原始类型后发现,其中包含String类型,于是调用toString全部转为string类型,得到'1'+'a'
// 4. 最终拼接出结果 '1a'
obj1+obj2
// 1.左右两边同时进行ToPrimitive()操作,根据上边的运算,我们知道obj1返回数值类型1,obj2返回是字符类型'a'
// 2.得到2个原始类型后发现,其中包含String类型,于是调用toString全部转为string类型,得到'1'+'a'
// 3. 最终拼接出结果 '1a'
例:==抽象相等比较
这种比较分为两类
- 类型相同
- 类型不同 类型相同时不会发生隐式转换,于是我们只谈类型不同之时,这里的规律比较复杂,不过只要牢记一个点,进行+运算时是
如果获取的原始值当中包含String,则对所有原始值执行toString处理后进行拼接
而进行逻辑运算时,刚好相反
如果获取的原始值当中包含String,则尽量将String转为Number类型后进行比较
因为毕竟比较的期望还是数值的比较
1.如果均为number类型,直接比较
1 == 2 // false
//两边都是数值类型,没有隐式转换
2.如果x为string,y为number,x转成number进行比较
'0' == 0 // true
// 其实就是我上边说的,如果有字符串将字符串转为数字 0 == 0 返回true
3.存在boolean,安装toNumber将boolean转为1或0,再进行比较
3 == true // false
// 还是获取原始值,3 == 1返回false
'0' == false // true
// 获取原始值,'0' == 0,将String转为Number,0 == 0返回true
4.如果存在对象,调用ToPrimitive获取原始类型,再进行比较
var obj = {
valueOf:function(){
return '1'
}
}
1 == obj // true
// 1. 两边同时调用ToPrimitive type为number进行转换,调用obj的valueOf方法,发现返回的是个原始值String类型
// 2. 于是对String类型进行处理,转换为Number类型
// 3. 1 == 1 于是返回 true
[] == ![] // true
// 1.[]作为对象ToPrimitive得到''
// 2.![]作为boolean转换得到0,有些同学可能会有疑问啊,[]为'',!''不应该为ture是1吗,其实不是,
// 我们知道引用类型[]其实是一个指向内存的指针,这个地址肯定是有的,于是!(内存指针)取反,则返回false,为0
// 3. '' == 0 发现包含字符串,于是toNumber转为number得到0 == 0 返回 true