史上最全隐式类型转换来啦!!!

290 阅读15分钟

目前在学前端,第一次写掘金博客,想与大家一起学习,是参考了掘金上一些优秀文章整合出来的,有什么问题小伙伴们可以留言啊!!!一起进步!!!

在js中隐式转换类型总是返回基本类型值(如数字,布尔值等),不会返回对象或者函数。

一、js内部用于实现类型转换的4个函数

在ECMAScript定义了4个抽象的操作,在js内部分别为:

  • ToPrimitive(input [,type]) 将值转换为基本类型值
  • ToString(argument) 将值转换为字符串类型的操作
  • ToNumber(argument) 将值转换为数字类型的操作
  • ToBoolean(argument) 将值转换为布尔值类型的操作

(一) ToPrimitive

JS中每个值隐含的自带的方法,用来将值(无论是基本类型还是引用类型)转换为基本类型值

如果值为基本类型,则直接返回值本身,如果值为对象,其看起来大概是这样:

Toprimitive(obj, type)

obj:需要转换的对象

type:期望的结果类型,type的值可以是number 或者 string, 默认情况下为 number,但 Date对象特殊,其默认为 string

  1. 当type 的值为number时规则如下:

    (1). 调用 obj的valueOf 方法,若为原始值,则返回,否则下一步

    (2). 调用 obj的 toString 方法,如果为原始值,则返回,否则下一步

    (3). 如果 toString方法 返回的是对象,且该对象不是自己重写的,则调用原型链顶端的toString方法,即Object.prototype.toString.call(对象) 如果是自己重写的,则报错,抛出TypeError异常

          const obj = {
            toString() {
              return 2
            },
            valueOf() {
              return 1
            }
          }
        Number(obj)       //返回值为 1,想转化为数字型,先调用valueof
  1. ** 当type 的值为string时规则如下:**

    (1). 调用 obj的 toString 方法,如果为原始值,则返回,否则下一步

    (2). 调用 obj的valueOf 方法,若为原始值,则返回,否则下一步

    (3). 抛出TypeError异常

      const obj = {
        toString() {
          return 2
        },
        valueOf() {
          return 1
        }
      }
    String(obj)     //'2'   想转化为字符型,先调用toString方法

注意为引用类型类型时,且没有重写 toString方法时,使用的是原型链顶端的toString方法,即Object.prototype.toString.call (对象)

(二) ToString

这里所说的ToString可不是对象的toString方法,而是指其他类型的值转换为字符串类型的操作。规则如下:

  • null:转为 "null"

  • undefined 转为 ”undefined“

  • 布尔类型:truefalse分别被转为"true""false"

  • 数字类型:转为数字的字符串形式,如10转为"10"1e21转为"1e+21"

  • Symbol() : 抛出异常

  • 数组:转为字符串是将所有元素按照","连接起来,内部调用数组的Array.prototype.join()方法,如[1, 2, 3]转为"1,2,3",空数组[]转为空字符串,数组中的nullundefined,会被当做空字符串处理

  • 普通对象:转为字符串相当于直接使用Object.prototype.toString(),返回"[object Object]"

        String(null)  //'null'
        String(undefined) //'undefined'
        String(true)    //'true'String(10) //'10'
        String(011) //'9'
        String(1e21) //'1e+21'String([1, 2, 3]) // '1,2,3'
        String([]) // ''
        String([null]) // ''
        String([1, undefined, 3]) // '1,,3'
        String({}) // '[object Object]'
        String({a:1, b:'22'}) // '[object Object]'// 拥有toString() 方法的值调用 String()函数时,实际上就是在调用 toString()方法,如String([])等价于[].toString()
        // null 和undefined没有 toString()方法,所以对这两个调用 toString()时会报错
注意:上面所说的规则是在默认的情况下,如果修改默认的`toString()`方法,会导致不同的结果

(三) ToNumber

指其他类型转换为数字类型的操作。

  • null:转为0
  • undefined: 转为 NaN
  • 布尔值:true 和 false分别被转为 1 和 0
  • 字符串:如果是纯数字,转化为对应的数字,空字符串(包括只有纯空格的字符串)转为0,否则一律按转换失败处理,转为 NaN
  • Symbol() :抛出异常
  • 数组:数组首先会被转为原始类型,也就是ToPrimitive,然后在根据转换后的原始类型按照上面的规则处理
  • 对象:同数组的处理
        Number(null) // 0
        Number(undefined) // NaN
        Number('10') // 10
        Number('10a') // NaN
        Number('') // 0 
        Number('    ')//0
        Number(true) // 1
        Number(false) // 0
        Number([]) // 0  []调用toString()转为原始数据:'',然后将其转为数字类型 0
        Number(['1']) // 1  ['1']调用toString()转为原始数据:'1',然后将其转为数字类型 1
        Number(['1','2']) //NaN ['1','2']调用toString()转为原始数据:字符串'1,2',然后转为NaN
        Number({}) // NaN  {}调用toString()被转为原始类型:字符串'[Object Object]',然后将其转为数字NaN
    //修改了默认的toString 和valueOf方法
      const obj1 = {
        valueOf () {
          return 100
        },
        toString () {
          return 101
        }
      }
      Number(obj1) // 100
    ​
      const obj2 = {
        toString () {
          return 102
        }
      }
      Number(obj2) // 102
    ​
      const obj3 = {
        toString () {
          return {}
        }
      }
      Number(obj3) // TypeError

(四) ToBoolean

指其他类型转换为布尔类型的操作。

js中的假值只有falsenullundefined''0NaN6个,其它值转为布尔型都为true

注意 带有空格的空串也转为 true.

      Boolean(null)        // false
      Boolean(undefined)    // false
      Boolean('')            // flase
      Boolean(NaN)         // flase
      Boolean(0)            // flase
      Boolean('  ')         //true
      Boolean([])           // true
      Boolean({})           // true
      Boolean(Infinity)    // true

二、隐式类型转换规则

2.1+操作符

  • 两边有至少一个为 String 类型时,两边的变量都会被隐式转换为字符串;
  • 当一侧为Number类型,另一侧为引用类型时,将引用类型和Number类型转换成字符串后拼接;
  • 其余情况均转换为Number

即如果 +操作符其中一个操作数为字符串(或者通过ToPrimitive操作之后最终得到字符串),则执行字符串的拼接,否则执行数字加法。

注意语句开始时为{ }时,会被解析为代码块,所以 {} + '1' 结果为 1,因为 {}被js解析器解析为代码块,实质上运算的是 +'1',所以结果为 1,如果用小括号括起来就可以了

        console.log(undefined + '2')   //'undefined2'  隐式转换,将undefined转换为字符串'undefined'
        console.log(['1',2,'2'] + '1')     //'1,2,21'
        console.log([] + '1')           //'1'   []-->''
        console.log(1 + '23')   //'123'
    ​
        console.log(1 + false)   //1
        console.log(true + false)   //1
    ​
        {} + []                     //0,  当语句开始为{时,会被JS解释器认为是代码块,所以实质上运算的是+[],将空数组转为Number,得0
        [] + {}                  //[object Object] 使用 ToPrimitive 方法,[]转为'',而{}转为[object Object],最终相加得[object Object]
    ​
        let a = {name:'hhh'}
        let b = {age:13}
        console.log(a + b)    //[object Object][object Object]
        
        console.log(1 + 1 + '23')    //'223'   从左向右 1+1结果为2,2+‘23’字符串拼接
        console.log(1 + '23' + 4 + 5)   //'12345'

两个操作数都是数值时,规则为:

  • 如果一个操作数为NaN,则结果为 NaN
  • 如果是 Infinity + Infinity,结果是 Infinity
  • 如果是 -Infinity + (-Infinity ),结果是-Infinity
  • 如果是Infinity + (-Infinity ),结果是NaN
  • 如果是 +0 + (+0),结果是 +0
  • 如果是(-0) + (-0),结果是 -0
  • 如果是 (+0) + (-0),结果是 +0

2.2 -、*、/ 操作符

会先将非Number 类型转换为数字

        console.log(25 - '23');  //2
        console.log(1 * false);   //0
        console.log(1 / 'aa');   //NaN  'aa'转为数字 NaN,所以 1/NaN 返回NaN
        console.log(2 * ['5'])   //10

2.3 == 操作符

1. 布尔类型和其他类型比较

只要布尔类型参与比较,会首先将 布尔类型的值转换为数字类型

    console.log(3 == true);  //false  3-->3, true --> 1
    console.log('0' == false);   //true  false-->0,  '0'-->0

2. 数字类型和字符串类型比较

当数字类型与字符串相等比较时,字符串类型会先被转换为数字类型

根据字符串的 ToNumber规则,如果是纯数字形式的字符串,则转为对应的数字,空字符串转为0,否则一律按转换失败处理,转为NaN。注意Number('Infinity') 转换为 Infinity数字

    console.log('0' == 0);  //true  
    console.log('' == 0);  //true 
    console.log('12a' == 12)  //false 
    console.log(Infinity == 'Infinity') //true
    console.log(Infinity == Infinity);  //true

3. 对象与原始数据比较

当对象类型 和 原始类型作相等比较时,对象类型会依照 ToPrimitive 规则转换为 原始类型。

      '[object Object]' == {} // true
      '1,2,3' == [1, 2, 3] // true
      
      [2] == 2 //true   先将 [2]使用toString转换为原始类型:'2',现在变为字符串和数字比较,所以将字符串'2'转为数字类型 2
      [null] == 0 // true  先将[null]使用toString()转换为原始类型:空串'',同样为字符串和数字类型比较,将字符串''转为数字类型为0,所以返回 true
      [undefined] == 0 // true 先将[undefined]使用toString()转换为原始类型:空串'',同样为字符串和数字类型比较,将字符串''转为数字类型为0,所以返回 true
      [] == 0 // true  []--> '' ,''--> 0
      
      const a = {
        valueOf () {
          return 10
        }
        toString () {
          return 20
        }
      }
      a == 10 // true  首先调用a 的valueOf方法得到原始数据 10

    > 对象的`ToPrimitive`操作会先调用`valueOf`方法,并且`a``valueOf`方法返回一个原始类型的值,所以`ToPrimitive`的操作结果就是`valueOf`方法的返回值`10`

4. null、undefined和其他类型比较

null 和 undefined 相等的结果为 true, ECMAScript规范中 规定null 和 undefined之间互相 宽松相等(==),并且也与其自身相等,但和其他所有值都不相等

    console.log(null == undefined); //true
    console.log(null == null); //true
    console.log(undefined == undefined); //true
    console.log(null == ''); //false
    console.log(null == {}); //false

5.如果一个操作值为NaN

如果一个值为NaN,则相等比较返回false(NaN本身也不等于NaN)

6.如果两个操作值都为对象

如果两个操作值都为对象(不会发生类型转换),则比较它们是不是指向同一个对象,即内存地址

2.4>、<、>=、<= 关系操作符

  1. 两边均为字符串时,则比较字符串的字符编码值

    可使用 String.prototype.charCodeAt()方法得到字符串给定索引处的字符编码值

        // 按照字符串的字符编码值
        console.log('c' > 'b');  //true
        console.log('de' > 'fg');   //false
        console.log('1' > '2')   //false
        console.log('2' > '12')   //true
  1. 只有一个操作数是数值,则将另一个操作值转换为数值,进行数值比较

    注:NaN是非常特殊的值,它不和任何类型的值相等,包括它自己,同时它与任何类型的值比较大小时都返回false。即任何关系操作符在涉及比较NaN时都返回 false。

        console.log('12' < 13);  //true
        console.log(false < -1);  //false
        console.log(NaN > 0)   //false   NaN与任何值比较都返回false
        console.log(Infinity > 0)  //true
  1. 如果有布尔值,将其转换为数值再执行比较

  2. 如果有对象,将其转化为原始值再根据前面的规则执行比较

        // 对象
        let a = {}
        console.log(a > 2);   //false,  转换过程,首先调用 valueOf()--> 对象 {}, 再调用toString() --> '[object Object]'
         
        console.log(a.valueOf())  //{}
        console.log(a.toString());  //'[object Object]'
        console.log(Number(a.toString()));  //NaN 

2.5 递增递减++、--(前置与后置)、一元正负 操作符

一元正负操作符:使用ToNumber,即调用Number()函数进行转换为数值型

    +[]          //0
    +true        //1
    +{}          //NaN,{}首先转化为原始类型:字符串'[Obeject Obeject]',故为NaN
    +undefined    //NaN
    +null        //0
    +'234'        //234
    +'234a'       //NaN   
    +'   23'       //23    
    +'   '         //0

自增、自减操作符:只针对变量(常量不可用)

a++、++a或者a--、--a在运算中等同于: a = a+1;或者 a= a-1; 如果此处是常量如: 3 = 3+1,常量是不允许赋值的。所以自增、自减只针对变量

前置自增、自减:符号在前,先变值再赋值

后置自增、自减:符号在后,先赋值再变值

自增自减操作符就是变量使用ToNumber()规则转换为数值类型,然后进行自增自减操作,最后变量变为数值变量。

  • 如果是包含有效数字字符的字符串,先将其转换为数字值(转换规则同Number()),再执行加减1的操作,字符串变量变为数值变量。

  • 如果是不包含有效数字字符的字符串,将变量的值设置为NaN,字符串变量变成数值变量。

  • 如果是布尔值,先将其转换为0或1再执行加减1的操作,布尔值变量变成数值变量。

  • 如果是浮点数值,执行加减1的操作。

  • 如果是对象,先使用ToPrimitive规则转换为原始类型,再加减1。对象变量变成数值变量。


        // 包含有效数字字符的字符串
        let n = '123'
        console.log(n++)     //123
        console.log(n);      //124  ,数值变量// 不包含有效数字字符的字符串
        let n1 = '12a'
        console.log(n1++)     //NaN
        console.log(n1);      //NaN   ,数值变量// 变量是布尔值
        let n2 = true
        console.log(++n2)     //2
        console.log(n2);      //2   ,数值变量// 变量是对象
        let n3 = {}
        console.log(++n3)     //NaN
        console.log(n3);      //NaN   ,数值变量

2.6 !、&&、|| 逻辑运算符

逻辑非 (!) 首先通过Boolean() 函数将操作值转换为布尔值,然后求反。

逻辑与(&&) 操作符,如果一个操作值不是布尔值,遵循以下规则进行转换:

  • 如果第一个操作数经Boolean()转换后为false,返回第一个值,否则返回第二个值(不是Boolean()转换后的值)
  • 多个操作符串联时,返回第一个虚值表达式,如果没有 找到任何虚值表达式,则返回最后一个真值表达式,采用短路来防止不必要的工作。

虚值只有6个(0,NaN,'',undefined,null,false)

    console.log( 1 && 2)        //2
    console.log('0' && 200);   //200
    console.log(NaN && {});    //NaN
    console.log("  " && true && 5);     //5

逻辑或(||) 操作符,如果一个操作值不是布尔值,遵循以下规则进行转换:

  • 如果第一个操作数经Boolean()转换后为true,返回第一个值,否则返回第二个值(不是Boolean()转换后的值)
  • 多个操作符串联时,返回第一个真值表达式,采用短路来防止不必要的工作。可以用于初始化函数中的默认参数值
    console.log( null || 1 || undefined)        //1
    function logName(name) {
        let n = name || "mark"   //用于初始化函数中的默认参数值name
        console.log(n)
    }

三、类型转换相关的题目

3.1 笔试题

      '2' > '10' == true       //true  '2'>'10' 大于号两边均为字符串,所以比较对应的字符编码,故为 true,变为true == true  故返回 true
    ​
      [] == ![]               // true  首先对[]取反为false 变为 [] == false,有布尔值时,先将布尔值转为数值型,所以首先将false转为数字0,[]转为原始类型 '',所以返回true
            
      [] == 0                 // true     [] --> '' --> 0  0==0 
      
      [2] == 2               // true      [2]-->'2'  变为'2' == 2
    ​
      ['0'] == false         // true      false-->0, ['0'] --> '0'
    ​
      '0' == false           // true      false-->0, '0'-->0
    ​
      [] == false           // true       false-->0,  [] --> '' -->0
    ​
      [null] == 0           // true       [null]-->''-->0
    ​
      null == 0             // false        null与除自身和undefined以外所有的数据都不相等
    ​
      null == false         // false
      
      [null] == false       // true
    ​
      [undefined] == false      // true
    ​
      undefined == false    // false
      
      []  == []             //false  两个对象进行相等比较,比较的是内存地址
      {}  == {}             //false
      {}  == !{}           //false    !{}转为false,再转为数值型为0,{}进行转换为原始类型:'[Object Obeject]' ,转为数值类型为NaN,  即判断 NaN == 0,返回false

3.2 变量a在什么情况下会执行输出语句打印1,即a==1 && a==2 && a==3

    let a = ?
    if (a == 1 && a == 2 && a == 3) {
        console.log(1);
    }

分析:这道题考查的知识点是:相等运算符(==)在作比较时会进行隐式转换,而如果操作数是引用类型,则会调用 toString()valueOf() 方法对引用类型数据进行隐式转换。

只要改变原生的 valueOf 或者 toString方法就可以达到效果。

    // 方法一:利用toString方法
    let a = {
        // 定义一个属性来做累加
        i:1,
        toString() {
            return a.i++
        }
    }
    if(a == 1 && a == 2 && a == 3) {
        console.log(1);
    }
    ​
    // 方法二:利用valueOf方法
    let a = {
        // 定义一个属性来做累加
        i: 1,
        valueOf() {
            return a.i++
        }
    }
    if(a == 1 && a == 2 && a == 3) {
        console.log(1);
    }
    ​
    // 方法三:利用 Symbol,改写ES6的symbol类型的toPrimitive
    let a = {[Symbol.toPrimitive]: ((i) => ()=> ++i)(0)}
    if(a == 1 && a == 2 && a == 3) {
        console.log(1);
    }
    ​
    //方法四:利用Object.difineProperty()
    let val = 0
    Object.defineProperty(window, 'a', {
        get(){
            return ++val
        }
    })
    ​
    //方法五:利用数组(非常厉害)
    let a = [1,2,3]
    a.join = a.shift
    if(a == 1 && a == 2 && a == 3) {
        console.log(1);
    }

方法三:ES6 中提供了 11 个内置的 Symbol 值,指向语言内部使用的方法。

Symbol.toPrimitive 就是其中一个,它指向一个方法,当该对象被转为原始类型的值时,会调用这个方法,并返回该对象对应的原始类型值。这里就是改变这个属性,把它的值改为一个闭包返回的函数。

方法四:a.join = a.shift 的目的是将数组的 join 方法替换成 shift 方法。因为数组在参与相等比较时也会通过 toString() 将数组转为字符串,而该字符串实际上是数组中每个元素的 toString() 返回值经调用 join() 方法拼接(由逗号隔开)组成。现在我们将 join() 方法替换为了 shift() 方法,也就意味着数组在通过 toString() 隐式转换后,得到是 shift() 的返回值,每次返回数组中的第一个元素,而原数组删除第一个值,正好可以使判断成立。

Array.prototype.toString() 会在内部访问 join 方法,不带参数 。覆盖一个数组实例的 join 也将覆盖它的 toString 行为。

当在稀疏数组上使用时,join() 方法迭代空槽,就像它们的值为 undefined 一样。

join() 方法是通用的。它只期望 this 值具有 length 属性和整数键属性。

3.3 a在什么情况下会执行输出语句打印1,即a===1 && a===2 && a===3

    if(a === 1 && a === 2 && a === 3) {
        console.log(1);
    }

=== 没有类型转换,但还是可以的。在Vue源码实现双向数据绑定中,就利用了 defineProperty方法进行观察,观察到数据的变化并实时反映到视图层。每一次访问对象中的某一个属性时,就会调用这个方法定义的对象里面的get方法。每一次改变对象属性的值,就会访问 set方法。

自己定义get 方法

        var b = 1
        Object.defineProperty(window, 'a', {
            get: function() {
                return b++
            }
        })
        var s = (a ===1 && a === 2 && a === 3)
        console.log(s);     //true

每一次访问 a 属性,a 的属性值就会 +1。proxy也是类似的方法,都可以实现

参考