一次搞懂JavaScript类型隐式转换:[] == ![]

1,659 阅读9分钟

前言

想必大家在面试当中,往往都遇到过类似于给你一段奇怪的代码[] == ![]让你写出输出结果的场景,本次就帮你一次搞懂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
如果为原始类型则转换为该参数的数值类型:

输入类型结果
UndefinedNaN
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方法时,步骤如下:

  1. 调用ToObject方法得到一个对象(其实就是包装类型,感兴趣的同学可以自己去了解下)
  2. 原始数据类型转换为对应的内置对象,引用类型(Object)则不变
  3. 调用该对象内置的valueOf方法(继承自Object.prototype对象) 不同内置对象的valueOf实现:
  • String => 返回字符串值
  • Number => 返回数字值
  • Date => 返回一个数字,即时间值,字符串中内容是依赖于具体实现的
  • Boolean => 返回Boolean的this值
  • Object => 返回this

隐式转换:重头戏来了 []==![]

前面讲了那么多概念,其实就是为了大家可以充分理解,在运算隐式转换时,js究竟都做了什么

例:+运算

  1. 在进行运算时+左右分别进行进行ToPrimitive()操作,获取原始值
  2. 如果获取的原始值当中包含String,则对所有原始值执行toString处理后进行拼接
  3. 其他的都进行toNumber处理
  4. 在转换时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