聊聊JS里的类型转换

390 阅读12分钟

js基础里面有关类型转换的文章其实都已经说烂了,但自己仍然模棱两可,尤其是一些奇怪的转化结果,有时候想不起来其中的原理,一知半解,没办法,一些基础还是需要多巩固。JS里面的类型转换一般讨论的是布尔类型、字符串、以及数值类型的转换,原始值之间的转换规则不多,涉及的更多的是引用类型。所以本篇会介绍基本的转换规则,以及JS里类型转换的原理——ToPrimitive抽象操作,并稍稍介绍了宽松等价==里面的比较规则。

本篇文章主要是阅读了霖呆呆的文章《从206个console.log()完全弄懂数据类型转换的前世今生》上和下两篇文章后自己再加以总结而来,ToPrimitive的原理同时参考了《你不知道的JS》(中卷),参考文章链接已放在文末。

1. 原始类型转换成布尔值

使用Boolean()进行类型转换

这个的转换规则最简单,Boolean强制转换规则:

false、undefined、null、+0、-0、NaN、"" false
除了上面的情况 true
Boolean(false); // false
Boolean(undefined); // false
Boolean(null); // false
Boolean(0); // false
Boolean(-0); // false
Boolean(NaN); // false
Boolean(""); // false

Boolean(-1); // true
......

2. 原始类型转换成字符串String

使用String()进行类型转换

ECMAScript 有 5 种原始类型(primitive type),即 Undefined、Null、Boolean、Number 和 String。ES6中还包括Symbol 。

// 原始类型:
String(undefined); // "undefined"
String(null); // "null"
String(true); // "true"
String(false); // "false"
String(3); // "3"
String(NaN); // "NaN"
String('abc'); // "abc"
String(Symbol(1)); // "Symbol(1)"

3. 原始类型转换成数字Number

3.1 使用Number()进行类型转换

先来看原始类型:

Number(undefined); // NaN
Number(null); // 0
Number(true); // 1
Number(false); // 0
Number(3); // 3
Number(NaN); // NaN
Number(''); // 0
Number('abc'); // NaN
Number(Symbol); // NaN
Number(Symbol(1)); // 报错,Uncaught TypeError: Cannot convert a Symbol value to a number

规律:

  • 纯数字的字符串,会被转为相应的数字
  • null转为0
  • Boolean类型转换为对应的1和0
  • Symbol会报错
  • 其它的基本类型,包括非纯数字的字符串、NaNundefined都会被转为NaN

引用类型:

3.2使用parseInt()parseFloat()进行类型转换

解释:

  • parseInt() 函数可解析一个字符串,并返回一个整数。

  • parseFloat() 函数可解析一个字符串,并返回一个浮点数。

解析规则:

parseInt()parseFloat()都有两个参数,一个string,一个是进制数radix,默认为10进制

  • 在没有指定radix或者radix为0的情况下,parseInt会按十进制进行转换,如:parseInt(3)
  • 以“0x”开头,parseInt会按十六进制进行转换;以0开头时ES5规定按照10进制解析
  • 如果都是字母, 返回:NaN
  • 如果都是数字,则返回整数
  • 如果字母和数字都存在
    • 以数字开头,则取截止到第一个字母出现之前的所有数字进行转换
    • 如果参数“string”,以字母开头,直接返回NaN (10进制中字母不是一个有效的的表示)
  • string头尾部空格将被自动除去
  • 如果解析的不是字符串,则将其转换为字符串再解析
  • 如果radix不是数值,会被自动转为一个整数。这个整数只有在2到36之间,才能得到有意义的结果,超出这个范围,则返回NaN。如果第二个参数是0、undefined和null则直接忽略。
  • 如果字符串包含对于指定进制无意义的字符,则返回NaN
  • 自动转为科学计数法的数字,parseInt会将科学计数法的表示方法视为字符串
  • 注意转换number类型的时候小数点也是识别不了的,会当作其他字母处理
parseInt(3); // 3
parseInt("0x11"); // 17 	    ----0x开头16进制
parseInt('abc'); // NaN 		----都是字母
parseInt(NaN); // NaN									----非字符串转换成字符串再解析
parseInt(false); // NaN
parseInt('10'); // 10
parseInt("011w");     // 11, 	----以数字开头截取到w前面
parseInt("w11"); // NaN			----以字母开头直接返回NaN
parseInt(11, 2);  // 3			----指定radix,按进制返回
parseInt(011, 2); // NaN  						----011转换成string是9,而9对于二进制是非法字符
parseInt('546', 2) // NaN							----5、4、6对于二进制是非法字符
parseInt('011'); // 11								----目前chrome、firefox、ie中的结果都是11,若想用8进制解析,请加上第二个参数radix
parseInt(011); // 9										----跟上一行不同,011是Number类型,011转换成string是9
parseInt(1.11, 10); // 1
parseInt("    011      ", 2); // 3	----头尾部空格将被自动除去
parseInt('10', 37) // NaN							----radix的值范围在2-36之间,超出这个范围,则返回NaN
parseInt('10', 1) // NaN
parseInt('10', 0) // 10								----radix参数是0、undefined和null则直接忽略
parseInt(1000000000000000000000.5) // 1, 等同parseInt('1e+21') 
console.log(parseInt("1.23a12")) // 1 ---- 返回整数
parseInt(".23"); // NaN
parseInt(1.5); // 1 		---- 截取到小数点之前,所以为1,不存在四舍五入

parseFloat与parseInt类似,但也有不同

与parseInt()不同的是,parseFloat()可以将字符串转换成浮点数;但同时,parseFloat()只接受一个参数,且仅能处理10进制字符串。 (1)字符串中的第一个小数点是有效的,而第二个小数点就是无效的了,因此它后面的字符串将被忽略。 (2)如果字符串包含的是一个可解析为整数的数(没有小数点,或者小数点后面都是零),parseFloat()会返回整数。

parseFloat(3); // 3
parseFloat("0x11"); // 0 								----与parseInt不同,不识别十六进制,所以只截取到x前面的0			
parseFloat('abc'); // NaN 		----都是字母
parseFloat(NaN); // NaN									----非字符串转换成字符串再解析
parseFloat(false); // NaN
parseFloat('10'); // 10
parseFloat("011w");     // 11, 	----以数字开头截取到w前面
parseFloat("w11"); // NaN								----以字母开头直接返回NaN
parseFloat(11, 2);  // 11		----忽略第二个参数
parseFloat(011, 2); // 9  		----忽略第二个参数
parseFloat(1.11, 10); // 1.11
parseFloat("    011      "); // 11	----头尾部空格将被自动除去
parseFloat(1000000000000000000000.5) // 1e+21, 等同parseInt('1e+21') 
console.log(parseFloat("1.23a12")) // 1.23
parseFloat(".23"); // 0.23

4. 原始类型转对象Object

使用Object()

先来看原始类型,原始类型使用Object()来转换类型,有包装对象的会返回它们的包装对象,没有的如undefined和null会返回空对象如:

// 原始类型:
Object(undefined); // {}
Object(null); // {}
Object(true); // Boolean {true}
Object(false); // Boolean {false}
Object(3); // Number {3}
Object(NaN); // Number {NaN}
Object('abc'); // String {"abc"} 
Object(Symbol(1)); // Symbol {Symbol(1)}
Object(10n); // BigInt {10n}

5. 对象转布尔值

还是按之前说的转布尔值的规则,除了false、undefined、null、+0、-0、NaN、""以外,其他数据转换成布尔值都是true;

所以,所有对象(包括数组和函数)都转换为 true。

6. 对象转字符串

所有对象除了null、undefined以外的任何值都可以调用toString()方法,通常情况下它的返回结果和String一样

使用toString

在来看对象转字符串前,可以先看看原始对象的使用:

// 原始类型:
undefined.toString(); // 报错
null.toString(); // 报错
true.toString(); // "true"
false.toString(); // "false"
3.toString(); // 报错,因为小数点属于数字类型
3.1.toString(); // "3.1"
(3).toString(); // "3"
NaN.toString(); // "NaN"
'abc'.toString(); // "abc"
Symbol(1).toString(); // "Symbol(1)"
  • 我们常容易搞混的就是StringtoString,这两者到底有什么区别呢?
    • String是一个类似于Function这样的对象,它既可以当成对象来用,用它上面的静态方法,也可以当成一个构造函数来用,创建一个String对象
    • toString是除了null、undefined之外的数据类型都有的方法,通常情况下它的返回结果和String一样。

引用类型的转化

toString的转化规则:

  • 数组的toString方法是将每一项转换为字符串然后再用","连接
  • 普通的对象(比如{name: 'obj'}这种)转为字符串都会变为"[object Object]"
  • 函数(class)、正则会被转为源代码字符串
  • 日期会被转为本地时区的日期字符串
  • 原始值的包装对象调用toString会返回原始值的字符串
[].toString(); // ""
[1, 3].toString(); // "1,3"
({}).toString(); // "[object Object]"
({name: 'obj'}).toString(); // "[object Object]"
(function() {console.log(1);}).toString(); // "function() {console.log(1);}"
new Date().toString(); // "Sat Apr 11 2020 17:37:33 GMT+0800 (中国标准时间)"
Number(2).toString(); // "2"
Number.toString(); // function Number() { [native code] }" ----Number未调用时属于一个构造函数,返回源代码字符串
['', ''].toString(); // ","
[' ', '  '].toString(); // " ,  " 							----注意是有空格的
new Map().toString(); // "[object Map]"	----Map本身没有toString的方法,所以调用的是Object.prototype.toString方法,返回的是类型,toString是通过继承Object的
new Set().toString(); // "[object Set]"	---- 同上

使用String

前面第2条已经说过原始对象使用String的方式了,下面单看一下各种对象形式:结果跟toString相同

String([]); // ""
String([1, 3]); // "1,3"
String({name: 'obj'}); // "[object Object]"
String(function() {console.log(1);}); // "function() {console.log(1);}"
String(new Date()); // "Sat Apr 11 2020 17:37:33 GMT+0800 (中国标准时间)"
String(Number(2)); // "2"
String(['', '']); // ","
String([' ', '  ']); // " ,  " 	----注意是有空格的
String(new Map()); // "[object Map]"
String(new Set()); // "[object Set]"

7. 对象使用valueOf转基本类型(主要是数字)

valueOf 是对象的一个方法,它的作用是把对象转换成一个基本数据的值

基本数据类型调用valueOf()

// 原始类型:
undefined.valueOf(); // 报错
null.valueOf(); // 报错
true.valueOf(); // true
false.valueOf(); // false
3.valueOf(); // 报错,因为小数点属于数字类型
3.1.valueOf(); // 3.1
(3).valueOf(); // 3
NaN.valueOf(); // NaN
'abc'.valueOf(); // "abc"
Symbol(1).valueOf(); // Symbol(1)

引用类型对象调用valueOf()

valueOf的使用规则:

  • 非日期对象的其它引用类型调用valueOf()默认是返回它本身
  • 而日期对象会返回一个1970 年 1 月 1 日以来的毫秒数
[].valueOf(); // []
{}.valueOf(); // {}
(function () {}).valueOf(); // ƒ () {}
/(\[|\])/g.valueOf() // /(\[|\])/g
new Date().valueOf() // 1586602681789

8. 对象转数字

仍然使用Number这个函数,Number的执行过程会先调用valueOf()后调用toString()

Number([]); // 0  ----[]调用valueOf返回本身[],调用toString()返回空字符串,最后再将空字符串""转为数字0返回
Number([1, 3]); // NaN ----valueOf返回本身[1,3],toString再转为"1,3",非纯数字的字符串使用Number返回NaN
Number({name: 'obj'}); // NaN ----valueOf返回本身,toString再转为"[object Object]",非纯数字的字符串使用Number返回NaN
Number(function() {console.log(1);}); // NaN ----同上
Number(new Date()); // 1586773492674 ----日期对象使用valueOf返回毫秒数,使用Number转换数字结果为一个数字
Number(Number(2)); // 2
Number(['', '']); // NaN
Number([' ', '  ']); // NaN
Number([0]); // 0 	---- 因为toString返回的为'0',使用Number结果为0
Number(new Map()); // NaN
Number(new Set()); // NaN
Number(Symbol(1)); // 报错

总结规则:

  • 如果对象具有 valueOf 方法,且返回一个原始值,则 JavaScript 将这个原始值转换为数字并返回这个数字

  • 否则,如果对象具有 toString 方法,且返回一个原始值,则 JavaScript 将其转换并返回。

  • 否则,JavaScript 抛出一个类型错误异常。

9. 强制转化的抽象操作--toPrimitive()

在进入toPrimitive这个话题之前,需要先科普一下,js内部用于实现类型转换的主要4个函数:

  • ToPrimitive ( input [ , PreferredType ] )
  • ToBoolean ( argument )
  • ToNumber ( argument )
  • ToString ( argument )

不过需要注意,这里说的ToString跟上文不同,指的是js引擎内部使用的函数,而不是定义在对象的那个函数。

上文说过的String和Number种转换方式,String是先调用toString再调用valueOf,Number是先调用valueOf再调用toString。实际上这些规则都是遵循JS的一套内部转换规则:toPrimitive()的执行规则。

下面来看一下这个神秘的toPrimitive()函数,该函数的形式如下:

toPrimitive(input,preferedType?)

第一个参数input是需要转换的值,第二个是可选参数表示需要转换成的类型(主要包括非object的那几种原始类型);PreferredType参数要么不传入,要么是Number 或 String

下面看一下这个函数的主要调用规则:

  1. 是否存在preferedType参数,
    1. 如果存在,再看是Number还是String;
    2. 如果不存在,判断input,
      1. 如果传入的是日期对象,则preferedType默认为Number类型,
      2. 否则preferedType为String
  2. 根据preferedType判断
    1. String,继续判断是基本类型还是引用类型
      1. 基本类型 ---- 直接返回
      2. 引用类型,则调用toString,判断结果
        1. 结果为基本类型:---- 直接返回
        2. 结果为引用类型,则调用valueOf,继续判断结果
          1. 结果为基本类型:---- 直接返回
          2. 结果仍然为引用类型,抛出异常
    2. Number
      1. 基本类型 ---- 直接返回
      2. 引用类型 ,调用valueOf,判断结果
        1. 结果为基本类型:---- 直接返回
        2. 结果为引用类型,则调用toString,继续判断结果
          1. 结果为基本类型:---- 直接返回
          2. 结果仍然为引用类型,抛出异常

当使用Number或者String时,会首先调用toPrimitive这个抽象操作转化成原始类型(或异常),再进行最后的Number或String来处理这个值。

10. ==中的类型转换

先抛出一段概念,使用==进行比较时,会有下面的转换规则:

  1. 等号两边的类型是否相同,相同时则直接进行比较 (当然此例存在例外,NaN永远不等于它自己,+0-0是相等
  2. 等号两边的类型不同
    1. 都是基本类型
      1. String与Number比较,会将string转换为Number以后进行比较
      2. 任何东西与Boolean,将boolean转换为Number再进行比较 , (所以不要在任何情况下,使用== true== false
      3. null与undefined,一方如果为null或undefined,另一方只要也为null或undefined,就相等
    2. 有一边是引用类型
      1. String或Number与对象比较,将对象转为对应的String或Number再比较
      2. Boolean与对象,同前面,任何东西与Boolean,将boolean转换为Number再进行比较
    3. 两边都是引用类型,判断它们是不是指向同一个对象
42 == '42'; // true , ---- number与string
42 == true; // false, ---- number与boolean
"42" == true; // false, ---- string与boolean ,true转化为number为1,变成string与number比较,"42"转为number类型42,再比较
"42" == false; // false, ---- 理由同上
null == undefined; // true
({a:1}) == ({a:1}); // false 
[] == []; // false
[] == ![]; // true  ---- 这里的![]被执行,转换成了Boolean类型false,boolean类型被转为number0;现在变成了一边是对象,一边是number;[]使用ToPrimitive转换为"",Number("")为0;最后相等

Tips:

  • 可以使用if(a == null) {}来判断null或者undefined;这比if (a === undefined || a === null) {}其实要合适

参考文章: