宽松相等与逻辑比较

首先来灵魂拷问一下,你知道下面这些表达式的结果吗?

"" == 0;
"0" == false;
"" == [];
0 == [];
![] == [];

上面的所有表达式结果都为 true,怎么样,你答对了吗?

如果你能准确又自信的说出所有结果的话,说明你已经对 JS 中的宽松相等和类型转换相当了解啦,这篇文章对你可能帮助不大。如果你对结果有一点含糊的话,不妨跟着我一起探索下其中的知识点吧!

抽象操作

如果读过 ES 规范文档的话,你会发现文档里充斥着各种复杂的运算,为了方便描述这些复杂运算,文档作者往往会给这些运算起个名字,方便在其他地方引用,这些复杂运算就是抽象操作。在真正开始介绍宽松相等的规则之前,让我们先来了解一下 ES 规范中几种和类型转换相关的抽象操作吧。

ToString

ToString 操作会将其他类型的值转换成字符串类型,该操作具有以下规则

  1. null -> “null”
  2. undefined -> “undefined”
  3. true -> “true”
  4. false -> “false”
  5. 数字的转换遵循通用规则,但极大或极小的值会采用指数形式
  6. 对于其他对象,若没有定义toString方法,则输出内部属性[[Class]]

其他几条规则都比较明确,让我们来重点关注下第 6 条。JS 中除了普通对象之外,大多数都定义了自己的toString方法,比如Function.prototype.toString会输出函数源码,Array.prototype.toString会把值转换成字符串后在每个值中间插入逗号输出。这时候要获取[[Class]]属性的操作就要借助Object.prototype.toString来实现了,也就是我们非常熟悉的Object.prototype.toString.call(obj)操作了。

ToNumber

ToNumber 操作会将其他类型的值转换成数字类型,该操作具有以下规则

  1. true -> 1
  2. false -> 0
  3. undefined -> NaN
  4. null -> 0
  5. 字符串的转换遵循通用规则,但以 0 开头的八进制数无法正确识别。注意,空字符串转换后为 0
  6. 对于其他对象,会对其进行 ToPrimitive 抽象操作,期待得到基本类型的值,然后对该基本类型的值进行 ToNumber 操作

这里有一个特殊的地方要注意,undefined会被转换成NaN,而null则会被转换成0。另外ToPrimitive具体对应什么样的操作呢?

ToPrimitive

ToPrimitive 操作会将其他类型的值转换成基础类型的值,该操作具有以下规则

  1. 调用对象的 valueOf()方法,如果返回基本类型值,则返回该值,否则继续
  2. 调用对象的 toString()方法,如果返回基本类型值,则返回该值,否则抛出 TypeError 异常

除非特意实现过,否则大部分对象的 valueOf 方法会返回自身,所以实际环境中,ToPrimitive 操作大部分时候是依赖 toString 方法完成的。注意,刚刚说的只是实践经验,具体操作时必须严格按照规范来。

ToBoolean

ToBoolean 操作会将其他类型的值转换成布尔类型值。首先 ES 规范中先规定了一个假值列表

  1. undefined
  2. null
  3. false
  4. ±0/NaN
  5. ""

于是 ToBoolean 操作的规则是这样的

  1. 假值列表中的值 -> false
  2. 其他值 -> true

很简单对吧,不在假值列表中的值就是 true。

宽松相等

说起宽松相等,大部分开发者对它深恶痛绝,因为有太多反直觉的结果,所以大部分代码规范都要求开发者只使用严格相等,不要使用宽松相等。甚至有开发者整理了 JS 中各种典型值的相等关系表,可谓非常壮观。

Equality in JavaScript

但我觉得逃避解决不了问题,知其所以然才能有效避坑,所以让我们来探究一下宽松相等的原理。

首先,宽松相等和严格相等的区别是,宽松相等允许对操作符两侧的值按照“一定规则”进行类型转换。注意这里的“类型转换”,看起来它就是“万恶之源”了。让我们一起看看刚刚提到的“一定规则”具体是什么样的

  1. 运算符两侧分别是字符串和数字时,对字符串进行 ToNumber 操作
  2. 运算符两侧分别是布尔值和其他类型值时,对布尔值进行 ToNumber 操作
  3. 运算符两侧分别是 null 和 undefined 时,返回 true
  4. 运算符两侧分别是对象和数字/字符串时,对对象进行 ToPrimitive 操作

了解完规则之后,我们来拿文章最开始的例子检验一下。就拿![] == []来说吧,它的结果为 true,很反直觉对吧,空数组居然和它的取反相等,让我们来看一下 true 是被怎么推导出来的。

  1. 对符号左侧表达式![]求值,由于[]不在假值列表里,所以![]的结果为 false
  2. false == []按照规则,需要对 false 进行 ToNumber 操作,结果为 0
  3. 0 == []一侧是对象,一侧是数字,所以对对象进行 ToPrimitive 操作
  4. [].valueOf()返回结果是[],不符合要求
  5. [].toString()返回结果是"",符合要求
  6. 0 == ""按照规则,对""进行 ToNumber 操作,结果是 0
  7. 0 == 0结果为 true

怎么样,对照规则一步步走下来,逻辑是不是清晰多了?

逻辑比较

所谓逻辑比较就是大于小于这些,逻辑比较中也是存在类型转换的。其转换规则如下

  1. 如果双方不都是字符串
    1. 对双方进行 ToPrimitive 操作
    2. 如果结果依然存在非字符串, 则对结果再次进行 ToNumber 操作,比较数字大小
    3. 如果结果均为字符串,则按字符串情况比较
  2. 双方都是字符串,则按字母顺序比较

值得注意的一点是,大于等于和小于等于是通过逻辑取反得到结果的,也就说(a >= b) === true成立的条件是,(a < b) === false成立。这可能会造成一些反直觉的结果。

var a = { n: 42 };
var b = { n: 43 };
a < b; // -> false
a == b; // -> false
a > b; // -> false
a <= b; // -> true
a >= b; // -> true

结果乍一看非常反直觉,前三个表达式说明 a 既不大于 b,也不小于 b,也不等于 b,但是后面的大于等于和小于等于却是 true。其实结合上面说的很容易理解,比如a <= b的结果是由a > b的结果推导出来的,因此才是 true。

总结一下,虽然 JS 中的宽松相等和逻辑比较有很多反直觉的结果,但其背后都有规则可循,掌握了这些规则之后,我们发现宽松相等也不是那么坑,比如obj == null就能一下子判断出 obj 是不是 null 或者 undefined 这两种情况,很实用,也很安全。希望大家看了这篇文章之后能摘掉有色眼镜来看待==运算符,并对它加以合理利用。


2265 Words

2017-10-03 21:19 +0800

comments powered by Disqus