糟粕真香:JS中的自动类型转换

现在你要告诉我,你究竟为了我哪一点坏处而开始爱起我来呢?——威廉·莎士比亚,《无事生非》(Much Ado About Nothing)

《Javascript: The Good Parts》一书中糟粕部分的第一节就是说的邪恶的==号: JavaScript有两组相等运算符,=== 和 !==,以及它们的邪恶的孪生兄弟 == 和 !=。=== 和 !==这一组运算符会按照你期望的方式工作。如果两个运算数类型一致且拥有相同的值,那么 === 会返回 true,!== 返回 false。而它们邪恶的孪生兄弟只有在两个运算数类型一致时才会做出正确的判断,如果两个运算数是不同的类型,它们试图去强制转换值的类型。转换的规则复杂且难以记忆。作者举了这些例子:

'' === '0'           // false
0 == ''              // true
0 == '0'             // true

false === 'false'    // false
false == '0'         // true

false == undefined   // false
false == null        // false
null == undefined    // true

' \t\r\n ' == 0      // true

我承认,看完这些例子我已经懵了,特别是最后一个,那是什么鬼?我知道这就是比较两个值时发生的自动类型转换,是JS里的糟粕,全世界没有几个人能记得住。但是,因为面试肯定会考因为我是一个求知若渴的人,我决定挠着头皮来一探究竟。

第一个问题,怎样的情况下会发生自动类型转换?

上文中的用 == 或 != 比较两个值是一种情况。其他还有哪些情况呢?我凭有限的经验可以回忆出:“数字 + 字符串会让数字变成一个字符串”,那么可以说用二元操作符操作两个值时会发生自动类型转化。然后呢,没了,我只记得这个。我还记得一个加号单元运算符可以让字符串变成数字,但是我觉得那是一个手动类型转换。既然记不得,那么我就去查查资料,经过CSDN掘金知乎博客园几分钟的洗礼,我发现了一个惊人的事实,如果不考虑那些“拆箱装箱”之类你心知肚明的自动类型转换,JS只在两种情况下发生自动类型转换。即:

  1. 用 == 或 != 比较两个值
  2. 用二元操作符操作两个值

比想象中简单多了有没有。

第二个问题,自动类型转换的规则是什么?

幸运的是,关于 == ,MDN有一篇文章详细地讲解了其中的自动类型转换规则。还给出了一张表格: 非严格相等.PNG 机智的我把这个表格分成三个部分来分析,所以这张表就成了这样: 非严格相等的分类.PNG 我对三个部分的描述分别是:

  1. Undefined类型和Null类型与任何类型的值比较,结果直接确定。特例是当undefined或null居左,object居右,且object为“效仿undefined的角色”(如document.all)时,undefined == object的结果为true。虽然有特殊情况,但不涉及类型转换。
  2. 相同类型的值需要再将它们进行一次严格相等比较,严格相等不可能发生自动类型转换。
  3. Number、String、Boolean 以及 Object 类型的值两两比较时会先发生自动类型转换,再进行严格相等或者非严格相等的比较,非严格相等会再根据这张表的规律进行比较,直到得出最终结果。

接下来重点分析第三部分的规律: 首先我们脑子里要有一个清晰的概念,非严格相等的意义就是找出那些虽然类型不同,但是“我就是觉得它们差不多”的值。比如undefined和null——“我就是觉得它们相等啊,因为它们都是一种虚无缥缈的东西,都给人一种空虚的感觉。”——JS之父如是说。

这里的“空虚的感觉”,就是undefined和null非严格相等的媒介。那么剩下的Number、String、Boolean以及Object又该怎么判断它们是否非严格相等呢?你要说100和'100'都给我一种开心的感觉也行,但是 55 和 true 呢?true 和 'abc' 呢?显然这里靠感觉不行了。得给它们找到新的媒介。于是JS之父苦思冥想之后,找回了年少贪玩的自己——小孩子的世界里,相等的只有数字。于是数字就成了这四种非严格相等判断的媒介。

带着这个概念,我们来看表格第三行: 第三个单元格,A === ToNumber(B)。数值A碰到字符串值B,A已经是个数字,于是让B转成数字,再进行严格相等的比较。“我让你转成数字再跟我比较已经是放宽要求了,是一种不严格的比较,所以既然我们现在都是数字,就让我们来一场真正的较量吧。”A对B说到。 第四个单元格同上 最后一个单元格, 数值A遇到了对象B,因为大部分对象经过ToNumber转换之后都是NaN,所以这次JS之父再次开启天真模式,把数值A遇到的对象都想象成一个装箱对象,“与其跟一堆NaN比较,我还不如把它当成数值装箱对象,万一是的就赚了”。

来到第四行,前面两个转换都不难理解。关键是 String A == Object B,被转换成了 ToPrimitive(B) == A,注意此处是非严格相等。ToPrimitive(B)是没有问题的,但是为什么作为字符串的A没有被转成数字从而直接进行严格相等比较呢?这和上一行的最后一个单元格就产生了冲突。经过一番心理斗争,我还是没能自圆其说,于是我抱着试试看的心态,切换到了英文版MDN,结果发现英文版的表格长这样: 英文版表格.PNG 鉴于“英文文档大多比中文文档靠谱”,我选择根据这张表格重新思考。经过几分钟的重新思考,我才终于恍然大悟,原来JS之父表面上天真地将所有值都换成Number类型,同时还偷偷地给字符串的装箱Object开了一条生路

带着这张更加靠谱的图,我们再来看第三行:

最后一个单元格,A === Object B 变成了 A == ToPrimitive(B),我们可以想象:如果B是一个数值装箱对象,那么A == ToPrimitive(B) 和 A === ToPrimitive(B)的结果并没有区别。但是,如果B是一个字符串装箱对象,A === ToPrimitive(B)一定不相等, A == ToPrimitive(B)有可能相等, 比如A为100, B为'100'。这是字符串装箱对象的第一条生路:变成原始值之后,与数值非严格相等。

再来到第四行: 倒数第二个单元格,String A == Boolean B, 它们既不是数值也不是Object,所以都被无情地转成了数值。 最后一个单元格,String A == Object B 变成了 A == ToPrimitive(B),想象一下,如果B是一个字符串装箱对象, A == ToPrimitive(B) 就变成了 A === ToPrimitive(B),这就是字符串装箱对象的第二条生路:变成原始值之后,与另一个字符串严格相等。

第五行: 最后一个单元格,可以类比第三行最后一个单元格

第六行: 不难理解

未完待续。 本文有许多不严谨的推论,仅供消遣。 尊重JS之父。 理解万岁。