JavaScript数据类型隐式转换

引言——面试题

在讲JavaScript的数据类型隐式转换前,我们先看道面试题:

console.log(new String('abc') == true)

console.log({} == true)

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

结果是什么呢?

先把结果写下来,放在一边,然后继续看

一、js中的数据类型隐式转换的三种情况

  1. 转换为boolean类型
  2. 转换为number类型
  3. 转换为string类型

二、js中数据类型转换的规则

1、转为Boolean类型

  • 转换条件:

    数据在 逻辑判断逻辑运算 之中会隐式转换为boolean类型

  • 转换规则表
    数据类型转换为true的值转换为false的值
    String任何非空字符串空字符串(""或’')
    Number任何非零数字0和NaN
    Object任何对象null
    Undefined不适用undefined
  • tips
    • 注意字面量形式、包装器方式,new 方式的区别,new操作符创建的隐式转换为Boolean都是true

      var a = 'Davie'                  //申明的是一个string类型,它是一个基本类型
      var a = String('Davie')          // String()是一个包装类,用于将参数转换成string类型
      var a = new String('Davie')     //采用new方式时创建了一个object类型
      

      使用typeof验证上面的结论

      var a='Davie'
      console.log(typeof a) //string
      
      console.log(typeof String(a)) //string
      
      console.log(typeof(new String(a))) //object
      
    • 连续使用两个非操作符(!!)可以将一个数强制转换为boolean类型,这在开发之中比较实用。

2、转换为 string 类型

  • 简单数据转换字符串对照表:
    原始数据类型转换之后的值
    数字类型数字类型的字符表示
    null‘null’
    undefined‘undefined’
    布尔类型true变’true’,false变’false’

3、转换为 number 类型

  • 简单数据转换数字对照表:
    原始数据类型转换之后的值
    空字符 ''或""0
    非空字符串将字符内的数据内容变为数据,如果还有其他符号中文等转为NaN
    true1
    false0
    null0
    undefinedNaN
    NaN(不用转,typeof NaN 得到"number")

4、运行环境对数据类型隐式转换的影响

很多内置函数期望传入的参数的数据类型是固定的,如:alert(value)方法,它期望传入的value值是一个string类型,但是如果我们传入的是number类型或者object类型等非string类型的数据的时候,就会发生数据类型的隐式转换。这就是环境运行环境对数据类型转换的影响。

三、复杂数据类型的转换规则

复杂对象如何转换为简单值

一个复杂对象在转为基础类型的时候会调用ToPrimitive(hint)方法来指定其目标类型。如果传入的hint值为number,那么就先调用对象的valueOf()方法,调用完valueOf()方法后,如果返回的是原始值,则结束ToPrimitive操作,如果返回的不是原始值,则继续调用对象的toString()方法,调用完toString()方法之后如果返回的是一个原始值,则结束ToPrimitive操作,如果返回的还是复杂值,则抛出异常。如果传入的hint值为string,则先调用toString()方法,再调用valueOf()方法,其余的过程一样。

那么复杂对象是以什么标准来判断 ToPrimitive(hint) 操作传入的hint值到底是number还是string 呢?

  1. 如果运行环境非常明确的需要将一个复杂对象转换为数字则传入number如 Number(value) 和 +value 则传入number

  2. 如果运行环境非常明确的需要将一个复杂对象转换为字符串则传入string如String(value) 和 alert(value) 则传入string

  3. 如果是用+号连接两个操作数,操作数在确定其中只要有一个为字符串的时候另外一个操作数会转为字符串,ToPrimitive()会传入string,但是如果两个操作数都不能确定是字符串的时候则默认传入number(Date对象是一个例外,它会默认传入string)进行数据类型转换。

调用valueOf()返回的结果:

对象返回值
Array返回数组本身
Boolean布尔值
Date存储的时间是从1970年1月1日午夜开始级的毫秒数UTC,eg:(new Date()).valueOf() -->1626695004310 (相当于调用了getTime())
Function函数本身
Number数字值
Object对象本身(这是默认情况)
String字符串
undefined、Null对象没有valueOf方法(Math、Error貌似有)

调用toString()返回的结果:

对象返回值
Array以逗号分隔每个数组形成的字符串,约等于调用.join()
Boolean“true”或“false”
Date“Mon Jul 19 2021 18:46:05 GMT+0800 (中国标准时间)”
Function函数的文本定义
Number数字转成的字符串如"123"
Object“[object Object]” ,特例:用new关键字加上内置对象创建的Object类型数据,调用toString。eg: (new String(‘abc’)).toString() ==> ‘abc’ (相当于先给他拍成对应的基础数据类型再调用toString方法)
String字符串本身

四、操作符影响数据的类型转换

1、!转换规则

!会将后面的数据先转成布尔值,然后取反。例如:

var a; //a = undefined
var r = !!a; 
console.log(r) //false

!!{} // true
!!undefined // false
!!null // false
!!NaN //fales

2、 ==比较

比较操作符会为两个不同类型的操作数转换类型,然后进行严格比较。当两个操作数都是对象时,JavaScript会比较其内部引用,当且仅当他们的引用指向内存中的相同对象(区域)时才相等,即他们在栈内存中的引用地址相同。 ---- 引用自MDN

1. 类型相同的情况
  • 如果比较的是两个对象,则比较两个对象的指针是否指向同一个对象,例如:
var a = {}
var b = {}
a==b //false

很明显,ab在堆内存中是两个对象。二另一种情况:

var a = {}
var b = a;
a==b //true

这时,ab就指向了同一个对象,所以相等。

  • 其它的基本类型,如果是相同类型,则直接进行严格比较就好,没什么好说的。
2. 类型不同的情况

如果两边类型不同,则两边都尝试转成number类型。对于引用类型,先调用valueOf(),如果能转成数字,则进行比较。不能转成数字就调用toString()方法转成字符串。

var a = '123'
console.log(a == false)  //false,'123'转成数字是123,右侧转成数字是0,最终比较123==0
console.log(a==123)  //true,右边是数字,把左 边转成数字123

如果有一边是object类型:

var a = new String(123)
console.log(a==123) //true,a.valueOf()结果就是"123",最终比较的是"123"==123 => 123 == 123

再看一个:

var a = {} 
console.log(a == 1)
//上面a==1在js解释引擎中的执行过程如下:
//a.valueOf()获取到的不是基本类型,调用a.toString()得到'[object Object]'
'[object Object]'==1;
//两边类型不致,左侧转成数字
NaN==1;//false,NaN跟任何类型比较都为false
3. null、NaN、undefined

null、NaN、undefined和string、number、boolean、object类型比较时,都不做隐式转换,比较的结果直接为false。但是需要注意以下几个规则:

console.log(NaN==NaN) //false
console.log(undefined==null) //true
console.log(null==null) //true
console.log(null==undefined) //true
undefined == undefined //true
4. 面试题解析

搞清楚规则后,开头的面试题就很容易了:

第一题:

//问题1:
console.log(new String('abc') == true)
 
//step1:右侧转成数字1,变成:
  new String('abc')==1
  //step2 new String('abc').valueOf()不是数字也不是字符串,再调用toString()
 'abc' == 1
 //step3:字符串转数字
  NaN == 1 //false,NaN和任何类型比较都为false

第二题:

//问题2:
console.log({}==true)

//step1:右侧转成数字
{} == 1
//step2 {}.valueOf()不是数字也不是字符串,再调用toString()
'[object Object]' ==1  
//step3:字符串转数字
NaN == 1 //false,NaN和任何类型比较都为false

第三题:

//问题3:
console.log([]==![])

//step1:!优先级比==高,先转右边,[]是对象类型,转成布尔值为true,!true就是false
[]==false
//step2:右侧转成数字为0
[]==0
//step3:左侧是一个对象,valueOf()转出来不是字符也不是字符串,调用toString(),得到空字符串
'' == 0
//step4:字符串转成数字
0 == 0 //true
5. 总结
  • 类型相同
    • 基本类型,直接比较值
    • 引用类型比较指针
  • 类型不同,尝试转成number类型,
    • 先调用valueOf()转成number
    • 不行就再用toString()方法转成string
  • null、NaN、undefined单独一套规则

3、 比较运算符 > <

来,在来看一道题:

console.log('666' < '7')

正确答案是 true

这是因为字符串类型比较大小时,不进行类型转换,而是逐位比较ascii码,第1位不同则返回结果,否则继续比较第2位,直到某一位不同为止

ascii码比较 0-9 < A - Z < a - z , A = 65a = 97;

在比如使用数组的sort方法排序:

var a=[1,10,6,100].sort()

结果是:

[1, 10, 100, 6]

原因是sort()方法默认的比较规则会先把每个元素转成字符串,然后比较字符串的ascii码来确定先后顺序。

4、 加号+

  • 当+号作为一元操作符操作单操作数的时候,他就会将这个数转换为数字类型

    var a = 6;
    a = (+a);
    
  • 当+号作为二元操作符时

    为了方便描述,我们把+号左侧的值叫做A,右侧的叫做B:

    • 第一步:如果A和B都是number类型,直接相加;

    • 第二步:接下来看A和B两个操作数是否有一个是string类型,存在一个字符类型的话,那么另外一个操作数也会无条件地转换为字符串,然后连接;

          var a = '1';
          var b = a + 1;//b= '11'
      
    • 第三步:既不是number,也不是string, 两个操作数会隐式转换成数字类型(如果无法成功转换成数字,则变成NaN,再往下操作),再进行加法算数操作

      例如:如果是[1,2,3]+4这种情况下又会发生什么呢?

      转换规则如下:

      1. 能转换成数字,返回之

      2. 否则调用valueOf(),如果执行结果是基本类型,返回之;

      3. 否则调用toString(),如果执行结果是基础类型,返回之;

      4. 无法得到原始值,抛异常。

        计算 [1,2,3] + 1的过程:
        
        [1,2,3].valueOf() => [1,2,3]
        
        [1,2,3] + 1
        
        [1,2,3].toString() + 1;
        
        '1,2,3' + 1
        (此时得到了字符串,则去匹配加号两边又一边为字符串的规则)
        
        "1,2,31"
        
        

在隐式转换的过程中,随着转换得到的数据发生变化,转换的规则也在变化,不会一直保持最开始的转换规则 :具体情况如上例,转换到得到字符串这一步会按照字符串的计算法则得到计算结果

5、 减号-

除了加号外,减号也很神奇。

使用减号在做非数字类型的运算时,也会发生隐式类型转换.来看下面几个例子:

  1. true会转换成1
5 - true // 4 
  1. ''空字符串、null转成0
5 - '' //5
5 - null //5
  1. undefined和非空字符串转成NaN
5 - undefined // NaN
5 - 'a' // NaN
  1. 可以转成数字的字符串转成数字
5 - '1'  //4

五、其他

1、隐式转换应用

字符型转数值

var a = "6";

a -= 0;
a = (+a)

2、toPrimitive()函数是如何执行的

应用场景: 在JavaScript中,如果想要将对象转换成基本类型时,也就是所谓的拆箱时,会调用toPrimitive()。

函数结构: toPrimitive(input,preferedType?)

参数解释:

input是输入的值,即要转换的对象,必选;

preferedType是期望转换的基本类型,他可以是字符串,也可以是数字。选填,默认为number;

执行过程:

如果转换的类型是number,会执行以下步骤:

  1. 如果input是原始值,直接返回这个值;

  2. 否则,如果input是对象,调用input.valueOf(),如果结果是原始值,返回结果;

  3. 否则,调用input.toString()。如果结果是原始值,返回结果;

  4. 否则,抛出错误。

如果转换的类型是String,2和3会交换执行,即先执行toString()方法。

你也可以省略preferedType,此时,日期会被认为是字符串,而其他的值会被当做Number。

栗子

>[]+[]
>""

加号操作符会将preferedType看成Number,调用ES内部的toPrimitive(input,Number)方法,得到空字符串

>[]+{}
>"[object Object]"

最终会调用双方的toString()方法,再做字符串加法

>{}+[]
>0

加号运算符的定义是这样的:如果其中一个是字符串,另一个也会被转换为字符串,否则两个运算数都被转换为数字。而同时,javascript有这样的特性,如果{}既可以被认为是代码块,又可以被认为是对象字面量,那么js会把他当做代码块来看待。

这就很好解释了,{}被当做了代码块,只有+[],根据加法的定义,被转换为0,就得到了结果。

在操作符中,==,排序运算符,加减乘除,在对非原始值进行操作时,都会调用内部的toPrimitive()方法

参考链接:https://zhuanlan.zhihu.com/p/104362868

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐