隐式类型转换管窥

308 阅读6分钟

今天我们通过一道具体的题目来探析一些隐式类型转换。

一、题干

如何让条件成立,成功打印‘Hello World’?

if(b == 1 && b == 2 && b == 3) { console.log('Hello World!'); }

二、思路

刚开始看到这道题的时候因为看到了==内心马上想到了隐式类型转换,但是如果b是字符串的话肯定无法满足b == 1 && b == 2 && b == 3, 这个时候去mdn查看一下==隐式类型转换的规则:

相等运算符(==和!=)使用抽象相等比较算法比较两个操作数。可以大致概括如下:

  • 如果两个操作数都是对象,则仅当两个操作数都引用同一个对象时才返回true。
  • 如果一个操作数是null,另一个操作数是undefined,则返回true。
  • 如果两个操作数是不同类型的,就会尝试在比较之前将它们转换为相同类型:
    • 当数字与字符串进行比较时,会尝试将字符串转换为数字值。
    • 如果操作数之一是Boolean,则将布尔操作数转换为1或0。
      • 如果是true,则转换为1。
      • 如果是 false,则转换为0。
    • 如果操作数之一是对象,另一个是数字或字符串,会尝试使用对象的valueOf()和toString()方法将对象转换为原始值。
  • 如果操作数具有相同的类型,则将它们进行如下比较:
    • String:true仅当两个操作数具有相同顺序的相同字符时才返回。
    • Number:true仅当两个操作数具有相同的值时才返回。+0并被-0视为相同的值。如果任一操作数为>NaN,则返回false。
    • Boolean:true仅当操作数为两个true或两个false时才返回true。 此运算符与严格等于(===)运算符之间最显着的区别在于,严格等于运算符不尝试类型转换。相反,严格相等运算符始终将不同类型的操作数视为不同。

本题与b相比较的是1 2 3,这几个数字,如果b也是数字那么条件(b == 1 && b == 2 && b == 3)不可能成立(假设b为1,则b不可能为2),所以==的两个操作数一定是不同类型的:

相等运算符(==和!=)使用抽象相等比较算法比较两个操作数。可以大致概括如下:

  • 如果两个操作数都是对象,则仅当两个操作数都引用同一个对象时才返回true。
  • 如果一个操作数是null,另一个操作数是undefined,则返回true。
  • 如果两个操作数是不同类型的,就会尝试在比较之前将它们转换为相同类型:
    • 当数字与字符串进行比较时,会尝试将字符串转换为数字值。
    • 如果操作数之一是Boolean,则将布尔操作数转换为1或0。
      • 如果是true,则转换为1。
      • 如果是 false,则转换为0。
    • 如果操作数之一是对象,另一个是数字或字符串,会尝试使用对象的valueOf()和toString()方法将对象转换为原始值。
  • 如果操作数具有相同的类型,则将它们进行如下比较:
    • String:true仅当两个操作数具有相同顺序的相同字符时才返回。
    • Number:true仅当两个操作数具有相同的值时才返回。+0并被-0视为相同的值。如果任一操作数为>NaN,则返回false。
    • Boolean:true仅当操作数为两个true或两个false时才返回true。 此运算符与严格等于(===)运算符之间最显着的区别在于,严格等于运算符不尝试类型转换。相反,严格相等运算符始终将不同类型的操作数视为不同。

如果b为字符串或者Boolean值,b被类型转换的之后会变成一个确定的值,那么条件(b == 1 && b == 2 && b == 3)也不可能成立。所以只剩下最后一种可能,那就是b为对象,经过valueOf或者toString方法返回的值可以使得条件(b == 1 && b == 2 && b == 3)成立。

  • 如果两个操作数是不同类型的,就会尝试在比较之前将它们转换为相同类型:
    • 当数字与字符串进行比较时,会尝试将字符串转换为数字值。
    • 如果操作数之一是Boolean,则将布尔操作数转换为1或0。
      • 如果是true,则转换为1。
      • 如果是 false,则转换为0。
    • 如果操作数之一是对象,另一个是数字或字符串,会尝试使用对象的valueOf()和toString()方法将对象转换为原始值。

知道了这些之后就可以构造b了。

const b = { i: 1, toString: function() { return b.i++ } }

这样每次调用b == 1/ b == 2 / b == 3的时候都会执行toString方法,从而使得条件成立。

可以在控制台打印下试试:

image.png

三、补充

还记得mdn上的这句话吗?

  • 如果操作数之一是对象,另一个是数字或字符串,会尝试使用对象的valueOf()和toString()方法将对象转换为原始值。

这句话说明构造b的时候用valueOf也是可以的。但是如果toString和valueOf同时被改写了,最终会执行哪个呢?

image.png

可以发现,程序只执行了valueOf方法。这是为什么呢?原因是这样的:

有时我们需要将非数字值当作数字来使用,比如数学运算。为此ES5规范在9.3节定义了抽象操作ToNumber。其中true转换为1, false转换为0。undefined转换为NaN, null转换为0。ToNumber对字符串的处理基本遵循数字常量的相关规则/语法(参见第3章)。处理失败时返回NaN(处理数字常量失败时会产生语法错误)。不同之处是ToNumber对以0开头的十六进制数并不按十六进制处理(而是按十进制,参见第2章)。数字常量的语法规则与ToNumber处理字符串所遵循的规则之间差别不大,这里不做进一步介绍,可参考ES5规范的9.3.1节。对象(包括数组)会首先被转换为相应的基本类型值,如果返回的是非数字的基本类型值,则再遵循以上规则将其强制转换为数字。为了将值转换为相应的基本类型值,抽象操作ToPrimitive(参见ES5规范9.1节)会首先(通过内部操作DefaultValue,参见ES5规范8.12.8节)检查该值是否有valueOf()方法。如果有并且返回基本类型值,就使用该值进行强制类型转换。如果没有就使用toString()的返回值(如果存在)来进行强制类型转换。如果valueOf()和toString()均不返回基本类型值,会产生TypeError错误。

四、参考文献

  1. 《你不知道的JavaScript(中卷)》4.2.2

  2. 相等(==) mdn

本题题源来自朋友派大欣,特此感谢!

第一次写技术博文,欢迎大佬批评指正 ~~~ 如在工作中有这道题的应用场景欢迎分享,不胜感激!