了解JavaScript中的类型转换

143 阅读9分钟

1.什么是类型转换?

类型转换的定义很容易理解,就是值从一个类型转换为另一个类型,比如说从String类型转换为Number类型,'42'→42。但是,如果我们对JS中的类型转换规则了解的并不足够的话,我们就会遇到很多令人迷惑的问题,就好像如果没有学习物理和化学,生活中处处都是魔法一样。

JS中的类型转换大概可以分为显式类型转换隐式类型转换两种。两者的区别十分明显,比如显式类型转换'String(42)',我们可以明显的看出,这行代码就是为了将42转为String类型。然而隐式类型转换通常是某些操作的副作用,比如''+42,就没那么明显了,它是加法操作的副作用。

如果你没有理解JS中的类型转换,下面的几个例子可能会让你王德发。

//ex.1
console.log([] == ![]); //true

//ex.2
const a1 = {},
    a2 = {};
console.log(a1 < a2); //false
console.log(a1 == a2); //false
console.log(a1 > a2); //false
console.log(a1 >= a2); //true 
console.log(a1 <= a2); //true

//ex.3
const a3 = {
    i: 1,
    valueOf() {
        return this.i++;
    },
};

console.log(a3 == 1 && a3 == 2); //true

这些奇怪的现象都有背后的道理,当然,前提是你要理解它。

2.类型转换的抽象操作

在详细介绍显式和隐式类型转换之前,我们需要先知晓StringNumberBoolean之间类型转换的基本规则。ES5规范中定义了这些类型之间转换的抽象操作(或者说转换规则)。下面简单介绍几种抽象操作规则。

  1. ToPrimitive(负责对象转为基本数据类型)

    为了将值转换为相应的基本类型值,抽象操作 ToPrimitive 会首先检查该值是否有 valueOf 方法。如果有并且该方法返回基本类型值,就使用该值进行强制类型转换。

    如果没有就使用 toString的返回值(如果存在)来进行强制类型转换。 如果 valueOftoString返回基本类型值,会产生 TypeError 错误。

    例如:

    //ex.4
    const testObj = {};
    console.log(testObj.valueOf()); // {}  testObj本身
    console.log(testObj.toString()); //'[object Object]' string字符串
    /*
    	首先调用testObj的valueOf方法,返回值是testObj本身,不是原始(基本)值类型;所以调用testObj的toString方法,返回了字符串'[object Object]',是基本类型,所以使用'[object Object]'进行操作。
    
    */
    console.log('' + testObj); // '[object Object]'
    
    testObj.valueOf = () => 'valueOf';
    /*
    	此时调用testObj的valueOf方法,返回的是字符串'valueOf',是基本类型,所以会用该返回值进行操作,而不会再继续调用toString方法了。
    */
    console.log('' + testObj); // 'valueOf'
    
  2. ToString(负责非字符串转为字符串)

    a.当要转换的值是基本类型时,

    要转换的值转成String之后的结果
    null'null'
    undefined'undefined'
    true , false'true' , 'false'
    Symbol('example')仅支持显式类型转换得到'Symbol('example')'
    普通数字 0,1,2符合通用规则 '0','1','2'
    极大或极小的数字 1/10000000使用指数形式 '1e-7' ,'1e+21'

    b.当要转换的值是对象时,如果是显式类型转换,则调用该值的toString方法,如果是隐式类型转换,则先通过ToPrimitive操作转为基本类型,再根据规则a转为String类型。

    当需要转换的值的**valueOf方法和toString**方法没有被修改时,常用的对象转换如下:

    要转换的值转成String之后的结果
    Object'[object Object]'
    Array [1,2,3]用逗号分隔的每个数组元素,'1,2,3'
    数组元素是null或者undefined时, [null],[undefined]空字符串 ''
    Date美式英语日期格式的字符串,'Fri Oct 15 2021 14:04:28 GMT+0800 (中国标准时间)'
    Function表示函数源代码的一个字符串
  3. ToNumber(负责非数字转为数字)

    要转换的值转成Number之后的结果
    true1
    false0
    undefinedNaN
    null0
    string '42','42px'基本遵循数字常量的相关规则,处理失败时返回NaN 42,NaN
    symbol不可转换
    Object等复杂类型先通过ToPrimitive转为基本类型,再转成Number类型

    例如:

    console.log(Number([null])); //0
    // [null] 通过ToPrimitive转成基本类型是空字符串 '',空字符串转为Number得到0
    
  4. ToBoolean

    JavaScript 中的值可以分为以下两类: (1) 可以被类型转换为 false 的值:undefinednull+0、-0和NaNfalse'' (2) 其他(被类型转换为 true 的值),即除了上面的几个值以外的。

    要注意的是有一个概念称为假值对象,假值对象和普通对象的区别在于,当他被转为Boolean时,会得到false。这类情况非常少见,但是确实存在,比如document.all对象,console.log(!!document.all); //false

好了,目前我们已经对这几种抽象操作有一定了解了,下面就可以稍微深入一下类型转换了。

3.显式类型转换

  1. 字符串和数字之间的显式类型转换

    ​ 字符串和数字之间的类型转换应该是我们最常见的一种了,它们之间的显式类型转换时通过JS中的原生构造函数String和Number实现的,但是通过new机进行构造函数调用。这两个函数在进行类型转换时,都遵循ToStringToNumber抽象操作。

    例如:

    console.log(Number('3')); // 3
    console.log(String(3)); //'3'
    
  2. 显式解析字符串

    解析字符串中的数字(parseIntparseFloat)和将字符串转换为数字虽然得到的结果都是数字,但是二者的转换规则是不一样的。

    ​ a. 相对于Number而言,parseInt允许传入的字符串参数中含有非数字字符,解析按照从左到右的顺序,遇到非数字字符就停止解析。例如console.log(parseInt('10px')); //10 ,console.log(Number('10px'));//NaN

    ​ b. parseInt是针对字符串的函数,当传入的第一个参数不是字符串时,会先将其转为字符串再进行解析。

    ​ c. parseInt有第二个参数,代表解析时使用的进制,比如 console.log(parseInt('11', 8)); //9,用八进制对'11'进行解析。该参数的范围是2-36,传入范围之外的值(如果传入0,会被设为10进制),会返回NaN。

    在使用parseInt(str,radix)时,radix并没有指定默认值(只是大部分情况radix会被默认设为10),也就是说如果你不传入这个参数的话,得到的结果可能是你意料之外的。比如当str0x或者0X(大写的X)开头时,radix会被默认设为16,所以当 console.log(parseInt('0x11')); //17会得到17。

    有个很有趣的例子:console.log([1, 2, 3].map(parseInt)); //[ 1, NaN, NaN ],就是利用了这个进制参数,结果数组的三项分别是parseInt(1,0),parseInt(2,1),parseInt(3,2),在第一项中,因为进制传入了0,被当为10进制处理,所以返回了1;第二项中因为进制传入了1,不在合法范围(2-36)内,所以返回了NaN;第三项中,因为进制传入2,而要解析的参数是3(二进制只有0和1的表达才是合法的),所以返回了NaN

  3. 显示转换Boolean

    StringNumber一样, Boolean是显式的 ToBoolean 类型转换,遵循ToBoolean操作规则。不过一般!!的方法使用的比较多。例如:console.log(Boolean(1), !!1);

4.隐式类型转换

隐式类型转换一般是其他操作的副作用。

  1. 加法操作:

    如果操作数是对象,则先对象通过ToPrimitive操作转为基本类型,然后再继续操作。

    如果其中一个操作数是**字符串(String)类型,则将另一个操作数也转换为字符串(String)**类型,进行字符串拼接。

    如果操作数中没有字符串(String)类型,则将两个操作数作为**数字(Number)**类型,进行加法运算。

    例如:

    // 因为 'hello' 是字符串,所以把 1 转成了 '1' ,然后进行字符串拼接
    console.log(1 + 'hello'); // '1hello'
    //因为没有字符串类型的操作数,所以进行加法运算,将 true 转成数字类型为 1 ,所以结果为2
    console.log(1 + true); // 2
    //因为 [2] 是对象类型,将其通过 ToPrimitive 操作之后,转成字符串 '2' ,进而需要把 1 转成 '1' ,进行字符串拼接
    console.log(1 + [2]); // '12'
    
  2. 减法、除法、乘法等操作:

    如果操作数是对象,则先对象通过ToPrimitive操作转为基本类型,然后再继续操作。

    将两个操作数作为**数字(Number)**类型,进行运算。

    例如:

    // [3] - [2]  → '3' - '2' → 3 - 2 =1
    console.log([3] - [2]); // 1
    //3 - 1 =2
    console.log([3] - true); // 2
    
  3. 将其他类型隐式转换为Boolean的操作:

    当值作为判断条件时。如 if语句中的条件判断表达式; for 循环语句中的条件判断表达式;while 循环do..while循环中的条件判断表达式;三目运算符(?:) 中的条件判断表达式; 逻辑运算符 || 和 && 中的值作为条件判断表达式。该值会被隐式转换为Boolean类型进行判断,遵循ToBoolean操作规则。

  4. 宽松相等(==):

    在JS中进行关系比较时,宽松相等(==)经常被解释为:“只比较二者的值是否相等,而不比较类型”,其实这种解释有些问题,这种解释中的“值”,应该怎样理解呢?

    比如我在进行比较console.log(0 == ''); //true,0和空字符串'',二者的原始值和类型都不相同,但是却是宽松相等的。

    更为准确地解释,应该是“当进行宽松相等的关系比较时,如果二者类型相同,则仅比较二者的值是否相同即可;如果二者类型不同,则需要先进行类型转换为同一类型,再比较值是否相同”。

    也就是说,当两个类型不同的值进行宽松相等的比较时,会发生隐式的类型转换。

    宽松相等中,类型不同时的类型转换规则如下(假设两个操作数分别为x,y):

    1. 如果x和y的类型分别为字符串(String)数字(Number),则将字符串类型转为数字然后比较。

      //先将字符串 '1' 转成了数字 1 ,再进行比较 
      console.log(1 == '1');//true
      
    2. Boolean类型的操作数,都需要转成**数字(Number)**类型。

      //先将 false 转成了 0,然后再根据规则1,将 '0' 转成了 0,然后进行比较
      console.log('0' == false);//true
      
    3. 如果一方是对象类型,则需要通过ToPrimitive抽象操作规则,转成基本类型以后再继续比较。

      // 先将对象类型的数组 [1] 转成了字符串 '1' ,再根据规则1,将字符串 '1' 转成了数字 1,然后进行比较
      console.log([1] == 0);//false
      
    4. null和undefined是宽松相等的,可以说,在宽松相等的比较中,null和undefined是一回事。

    另外要了解的是:

    • NaN 不等于 NaN 。 • +0 等于 -0

  5. 大于(>),小于(<),大于等于(>=),小于等于(<=):

    在进行大于小于这两种比较时:

    a. 如果有对象类型,首先通过ToPrimitive操作转成基本类型,在进行比较。

    b. 如果两者都是字符串,则按字符的ASCII码值比较。

    c. 若果有一方不是字符串,则将二者都通过ToNumber抽象操作转为Number类型之后,再进行比较。

    在进行大于等于小于等于这两种比较时:

    大于等于代表着不小于,即 a>=b的结果是 !(a<b)。小于等于同理。

5.回顾

了解了JS类型转换规则之后,在回过头看之前的几个例子,想来我们都能够理解这些奇怪的现象了。

ex.1:

//ex.1
console.log([] == ![]); //true
//[] 通过ToPrimitive转成基本类型,得到 '',  ![]得到false。宽松相等比较规则,转成数字 0
// 即 ''==0 , 空字符串''有转成了数字 0 ,所以最终为true 

ex.2:

//ex.2
const a1 = {},
    a2 = {};
//a1 a2都是对象类型,通过ToPrimitive操作之后,结果都是 '[object Object]'
//显然 '[object Object]'<'[object Object]' 和 '[object Object]'>'[object Object]'都是false
console.log(a1 < a2); //false
console.log(a1 > a2); //false
//在进行宽松相等比较时,二者类型相同,不会进行类型转换,而是比较二者的值,a1,a2显然指向的地址不同,所以a1==a2 =也为false
console.log(a1 == a2); //false

//小于等于  大于等于 分别为 大于和小于的结果取反,所以都为true
console.log(a1 >= a2); //true 
console.log(a1 <= a2); //true

ex.3:

//ex.3
const a3 = {
    i: 1,
    valueOf() {
        return this.i++;
    },
};

console.log(a3 == 1 && a3 == 2); //true

//a3是对象类型,通过ToPrimitive操作,转成基本类型得到 a3.i (即1),所以 a3==1 为true
//但是在上面执行valueOf的时候,i自增了一次,所以在 a3==2的比较时, a3转为基本类型的值时,得到的就是2了,所以a3==2也为true。