有趣的JS-隐式类型转换

我来自伯纳乌

有趣的JS-隐式类型转换

当两个不同数据类型的操作数在做运算,或者操作数与操作符不匹配的时候,js引擎不会报错,会把操作数转成对应的数据类型继续执行下去,这个转换是自动完成的,经常被叫做隐式类型转换。其实大部分开发者都或多或少了解过这一点,比如我们经常会写这样的表达式!!param来确保参数是个Boolen值,+param确保是个数字,param + ''把参数转为字符串,但是总会遇到一些更复杂的表达式,例如:
{1} + 3
3 + [1, 2]
'1' + +'3'
false == ![]
等等一大堆,那只要把规则理清楚了,这些问题就迎刃而解了。主要就是这几种情况(忽略BigInt和Symbol这两种不常用的类型):

1. +符号运算

这个运算符比较特殊,先把它给拎出来,因为它既可以表示字符串连接符号用来拼接两个字符串,又可以作为算数运算符做加法运算。这取决于运算符两边的数据类型

对于基本类型:

  1. 只要有一个是字符串:把另外一个操作数的数据类型转成String按照字符串拼接
  2. 否则把两个操作数转成Number类型做加法运算

对于复杂数据类型:先把复杂数据类型转为基本类型,再按上面的规则计算(具体怎么转再继续往下看)

转换规则是重点,在介绍转换规则之前先说下JS的数据类型,数据类型分为简单数据类型和复杂数据类型,Undefined、Null、String、Number、Boolen这5个属于简单的简单数据类型,它们的换规则如下:

类型转Number转String
UndefinedNaN"undefined"
Null0"null"
Booleantrue => 1; false => 0true => 'true'; false => 'false'
Number原样输出正数 => '正数'; 负数 => '负数'; NaN => 'NaN'; +0,-0 => '0'; Infinity => 'Infinity'; -Infinity => '-Infinity'; 科学计数法表示 => 对应的字符串; 非十进制数表示 => 十进制表示的字符串
String基本是跟Number转字符串相反的,但是8进制数字字符串会转成去掉开头0后的10进制数,16进制数字字符串 => 10进制数字;其他不转为数字的会转为NaN原样输出

对象的转换就比较复杂了,不会进行直接转换而是通过一系列的方法。
转Number:

  1. 先调用它的valueOf方法,如果是简单类型则执行运算,返回结果
  2. 否则调用它的toString方法,如果是简单类型则执行运算,返回结果
  3. 否则抛出错误Uncaught TypeError: Cannot convert object to primitive value

转String,如果对象是日期Date类型

  1. 先调用它的toString方法,假如返回是简单类型则执行运算,返回结果
  2. 否则调用它的valueOf方法,如果是基本类型则执行运算,返回结果
  3. 否则抛出错误Uncaught TypeError: Cannot convert object to primitive value

现在可以补充下复杂数据类型的转换规则了:

  1. 如果要转换的类型明确了,按明确的类型处理。比如'字符串' + obj,那么应该按字符串处理
  2. 默认是按Number类型转换,即先调用valueOf方法,如不满足再调用toString方法,还不满足抛出异常。如果是Date类型则默认是按String类型转换,即先调用toString再调用valueOf

补充一点:如果是数组转成String会先调用join方法再调用toString

let obj = {};
obj.valueOf = function() {return 3};
obj.toString = function() {return '1'};

+obj // 类型明确了,按Number处理,输出 3
'1' + obj // 类型明确了,按String处理,输出 '13'
obj + obj // 按默认处理,输出6

obj.valueOf = function() {return {}};
+obj // 类型明确,按Number处理,先调用valueOf,仍是对象,调用toString,输出 1

let date = new Date();
date.valueOf = function() {return 3};
date.toString = function() {return 1};

+date // 输出 3
'1' + date // 输出 '11'
date + date // 按默认处理,输出 2

date.toString = function() {return {}}; 
'1' + date // 类型明确,按String处理,先调用toString,仍是对象,调用valueOf,输出 '13'

ES6对象新加了Symbol.toPrimitive方法,假如对象存在这个方法则执行这个方法转换,没有的话执行上面的规则。toPrimitive方法只有一个参数,会根据传入的类型转成对应的结果,这里有一点需要注意:当作为字符串拼接的时候传入的参数是default,String(params)调用的时候参数才会传入string。对于Date类型应该是内部私有逻辑,试了下重写不管用,可以看下这个:https://tc39.es/ecma262/#sec-...

let obj = {};
obj.valueOf = function() {return 3};
obj.toString = function() {return '1'};

obj[Symbol.toPrimitive] = function(preferType) {
    if (preferType == "number") {
      return 666;
    }
    if (preferType == "string") {
      return '3';
    }
    return null;
}

+obj // 输出 666
'1' + obj // 输出 '1null',preferType是default
String(obj) // 输出 '3'

2. 关系运算符:>、<、==、===、>=、<=,!=

2.1相等运算符

对于===运算符除了比较值以外还会进行类型的比较,如果两边类型不一样肯定是不相等了
对于==号运算符:

  1. 如果两边都是引用类型(Object),存储的是内存地址,是不相等的([] == [] // false, {} == {} // false)
  2. 如果其中一个是null另一个是undefined返回true
  3. 如果其中一个是null或者undefined另一个不是null也不是undefined,返回false
  4. 如果其中一个是NaN,返回false
  5. Infinity除了与Infinity和+Infinity相比是true外,其他都是false
  6. -Infinity除了与-Infinity相比是true外,其他都是false
  7. 剩下的情况会把两边的操作数按上面提到的转Number的规则转成数字做比较,但如果两边的操作数都是字符串则按照从左到右依次按照它们的ASCII码值比较。例如'ac' < 'ab'结果是false,因为字母c的ASCII码值要大于字母b的ASCII码值

    2.2其他关系运算符

    按照2.1中规则7处理

3. 逻辑运算符(三目运算符、||、&&、!)、if和while条件表达式

转成Boolean值运算,除了这几个值undefined、null、0,+0、-0、''、NaN以外都是true

4. 算数运算符(*、/、-、%、++、--)和位运算符(|、&、~)

按照上面提到的转Number的规则,转成数字运算

其他情况

除了前面几种主要的情况,就剩下一些特例了

  • alert,这个函数会把参数转为字符串
  • {开头的语句会被当做代码块,跟上后面的语句的时候相当于一目运算符,按照一目运算符的规则执行。例如:{} + 1 相当于 +1。如果想把它按对象执行的话需要加个小括号当作表达式执行({}) + 1,输出"[object Object]1"
  • null <= 0是true,null >= 0也是true。因为null > 0是false,所以nul <= 0,同理null >= 0

推荐几个链接:
https://tc39.es/ecma262/#sec-toprimitive
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toPrimitive
https://interglacial.com/javascript_spec/a-11.html#a-11.6.1

阅读 1.3k

有趣的JS
把JS里那些有趣的东西记录下来,有的可能是设计的失误,有的可能由于历史遗留问题,还有可能是一些不得...

喜欢钓鱼、爱踢球的程序猿一枚

298 声望
9 粉丝
0 条评论

喜欢钓鱼、爱踢球的程序猿一枚

298 声望
9 粉丝
文章目录
宣传栏