JavaScript中的隐式类型转换

2,828 阅读9分钟

将值从一种类型转换为另一种类型通常称为类型转换,这是显式的情况。隐式的情况称为强制类型转换,在JavaScript中存在很多隐式类型转换的情况

ECMAScript数据类型

在ECMAScript中一共有7种数据类型 :

6种原始类型(基本数据类型):

  • Boolean
  • Null
  • Undefined
  • Number
  • String
  • Symbol (ECMAScript 6)

Object (引用类型):

  • Object 类型
  • Array 类型
  • Date 类型
  • RegExp 类型
  • Function 类型

基本数据类型是如何操作属性和方法的?

基本类型(基本数值、基本数据类型)是一种既非对象也无方法的数据。在 JavaScript中,共有6种基本类型:string,number,boolean,null,undefined,symbol (ECMAScript 6)。

那为什么基本数据类型没有属性和方法,我们仍然可以对其进行一系列的操作呢?

这里就要提到原生类型内建的包装对象,包括:

  • String()
  • Number()
  • Boolean()
  • Array()
  • Object()
  • Function()
  • RegExp()
  • Date()
  • Error()
  • Symbol()

在操作原始数据类型的属性和方法时,JS会自动将原始类型转换成一个对应的包装对象。此时该对象就拥有了属于它本身的一系列属性和方法。所以当我们定义了一些基本数据类型,想访问其对应的属性或方法时,就需要将其转换为这个值的包装对象,然而现实中并不需要这么麻烦,这是因为JS运行的宿主对象(浏览器等)将会自动地包装基本类型值来满足这样的访问。所以我认为所谓JS万物皆对象,或许只是自动的实现了一些隐式的转换。

binlive前端开发,web开发,node,vue,react,webpack

严格相等(===)和宽松相等(==)

关于 === 和 == 比较操作符的不同,在比较浅显的认知中,全等操作符(===)会比较类型,而比较运算符(==)则不会比较类型。 然而我们看一下其在MDN中的定义:

全等操作符(===)比较两个值是否相等,两个被比较的值在比较前都不进行隐式类型转换。 相等操作符(==)比较两个值是否相等,在比较前将两个被比较的值转换为相同类型。

由此得出这两个操作符最大的不同,就是在比较前是否会进行隐式类型转换

我们也经常会见过很多隐式转换比较的题目,如:

undefined == null //true
[] == [] //false
[] == ![] //true
{} == !{} //false
![] == {} //false
[] == !{} //true
[1,2] == ![1] //false

所以上述的是依据什么规则如何进行转换?在ECMA文档中的抽象相等比较算法具体描述了如下规则。

抽象相等比较算法

在比较x == y,其中 x 和 y 是值,结果返回 true 或 false 。比较规则如下:

1.当 x 的类型与 y 的类型相同时:

a. 如果x的类型为Undefined,返回true。
b. 如果x的类型为Null,返回true。
c. 如果x的类型为Number,则如下:
    i.   如果 x 为 NaN, 返回 false.(NaN == NaN 为false)
    ii.  如果 y 为 NaN, 返回 false.	
    iii. 如果 x 和 y 的值相同, 返回 true.
    iv.  如果x为+0,y为−0,返回 true.
    v.   如果x为−0,y为+0,返回 true.
          其他返回 false.
d. 如果x是string类型,那么假如x和y是完全相同的字符序列(相同的长度和相应位置的相同字符),则返回true。否则,返回false。
e. 如果x是布尔类型,假如x和y都为true或都为false,则返回true。否则,返回false。
f. 如果x和y引用同一对象,则返回true。否则,返回false

2. x 和 y 类型不相同的情况:

  2. 如果x为null,y为undefined,则返回true3. 如果x为undefined,y为null,则返回true。(其余的任何类型与undefinednull相比都为false)
  4. 如果x是数字类型,y是字符串类型,则返回x == ToNumber(y)的比较结果.
  5. 如果x是字符串类型,y是数字类型,则返回ToNumber(x) == y的比较结果.
  6. 如果x是布尔类型,则返回ToNumber(x)==y的比较结果.
  7. 如果y是布尔类型,则返回x == ToNumber(y)的比较结果.
  8. 如果x是字符串或数字类型,y 是对象类型,返回比较结果x== ToPrimitive(y)。
  9. 如果x是对象类型,y是字符串或数字类型,返回比较结果ToPrimitive(x)==y。
  10. 其他则返回false
上述规则看起来很复杂,但总结起来其实只有以下几点:
  1. 两个引用类型比较,只需判断它们是不是引用了同一个对象,是返回true,否则为false。
  2. undefined 和 null 两者互相比较或者与自身比较,结果是true。它俩与其他任何值比较的都为false。
  3. NaN与任何值比较包括它自身结果都是false。
  4. 引用类型和基本数据类型(String、Number)进行比较,引用类型会转换成与之所比较的基本数据的类型,再进行比较。
  5. 引用类型和基本数据类型(Boolean)进行比较,Boolean类型先转换为数字类型,引用类型再转换成与之所比较的基本数据的类型进行比较。
  6. String,Boolean,Number中的任意两个进行比较,最后都会转为Number类型再进行比较。

现在我们再来看一下具体的类型转换过程

1. ToNumber

抽象操作ToNumber将非数字值转换为数字类型。

  • 其中布尔类型 true 转换为1,false 转换为0。
  • 字符串类型,""(空字符串)转为0,'123' 转为 123,'123px' 则被转为NaN(按照上述总结,NaN与任何类型比较都为false,此时就不需要再考虑其他情况),
  • 特殊的,undefined 会转换为 NaN,null 会转换为 0,(按上述总结第3条,null 和 undefined 跟其他任何非该两个类型中其一的比较都为false。结果容易得出,就不需要再考虑其他类型转换了)。
  • 至于引用类型,它们都需要先进行ToPrimitive转换为基本数据类型后再转换为数字类型。

2. ToPrimitive

抽象操作ToPrimitive将引用类型转为基本数据类型。

JS引擎内部转换为原始值ToPrimitive(obj,preferredType)函数接受两个参数,第一个obj为被转换的对象,第二个preferredType为希望转换成的类型(默认为空,接受的值为Number或String)
在执行ToPrimitive(obj,preferredType)时如果第二个参数为空并且obj为Date的实例时,此时preferredType会被设置为String,其他情况下preferredType都会被设置为Number如果preferredType为Number。
转换为Number数字类型的过程为首先调用obj.valueOf(),如果执行结果是基本数值类型就返回该值,否则就调用obj.toString(),返回该字符串类型值。
转换为String数字类型的过程为首先调用obj.toString(),如果执行结果是基本数值类型就返回该值,否则就调用obj.valueOf(),返回该字符串类型值。

上述规则总结起来基本就是:

  1. ToPrimitive转换为原始数据类型时,如果是基本数据类型就直接返回该类型值。
  2. 如果不是,则优先调用valueOf方法(如果有),看其返回结果是否是基本类型,如果是,则返回。否则,再调用toString方法,转换为字符串类型后返回。
  3. 当类型为Date日期类型时,JS希望其优先被转换成字符串类型,则先调用toString方法,转换为字符串。
  4. 其他情况报错。因为从ES5开始,使用Object.create(null)创建的对象由于没有原型链,自然就也没有valueOf()和toString()方法,这种情况出现的很少,基本可以忽略。

binlive前端开发,web开发,node,vue,react,webpack

在上图测试结果总结如下:

  • 有所的object类型转换都会转换为"[object Object]"字符串
  • 空数组[]会被转换为空字符串'', [1, 2, 3]会被转换为字符串'1,2,3',复杂类型的数组[{a: 1, b: 2}]会被转换为"[object Object],[object Object]"
  • 日期对象会转换为字符串格式,如 new Date() 会转换为'Wed Apr 17 2019 19:52:56 GMT+0800 (中国标准时间)'

由此可以看出,对象、数组、日期类型最后基本都被转换为字符串类型。所以我们姑且可以将ToPrimitive看做将引用类型最后都转换为字符串类型。

3.ToBoolean

抽象操作ToBoolean将转为布尔类型。
对于布尔类型转换来说,假值(undefined、null、false、+0、-0、NaN、"")会被转换为false,除假值外的所有值都会被转换成true。

4.ToString

抽象操作ToString将转为字符串类型。
对于字符串类型也比较好理解,大部分的基本数据类型转换为字符类型时,基本就是在它基础上加了引号这么直观,比较特殊的就是+0、0、-0它们几个都会被转成相同的'0',还有那些极小和极大的数字将会转换为指数形式的字符串来表现,而引用类型转换成字符串的时候也跟它们ToPrimitive时候的规则一致(见上文ToPrimitive)。

常见的隐式强制类型转换

  1. + - * / % 这几个运算符都可以将结果转为数字类型
  2. + 运算符常用来进行数字类型强制转换,因为它不会像**-**运算符一样影响结果
  3. + 运算符可以将日期类型new Date()转换成数字类型的时间戳
  4. ! 逻辑运算符可以将任何类型转为布尔类型,因为!运算符转换后的结果是被反转的,我们也可以使用两个!来得出正确的结果
  5. ** if()、 while() **括号内可以对所有数据类型默认执行强制布尔类型转换
  6. 三元运算符的条件也会执行强制布尔类型转换