宽松相等与逻辑比较
首先来灵魂拷问一下,你知道下面这些表达式的结果吗?
"" == 0;
"0" == false;
"" == [];
0 == [];
![] == [];
上面的所有表达式结果都为 true,怎么样,你答对了吗?
如果你能准确又自信的说出所有结果的话,说明你已经对 JS 中的宽松相等和类型转换相当了解啦,这篇文章对你可能帮助不大。如果你对结果有一点含糊的话,不妨跟着我一起探索下其中的知识点吧!
抽象操作
如果读过 ES 规范文档的话,你会发现文档里充斥着各种复杂的运算,为了方便描述这些复杂运算,文档作者往往会给这些运算起个名字,方便在其他地方引用,这些复杂运算就是抽象操作。在真正开始介绍宽松相等的规则之前,让我们先来了解一下 ES 规范中几种和类型转换相关的抽象操作吧。
ToString
ToString 操作会将其他类型的值转换成字符串类型,该操作具有以下规则
- null -> “null”
- undefined -> “undefined”
- true -> “true”
- false -> “false”
- 数字的转换遵循通用规则,但极大或极小的值会采用指数形式
- 对于其他对象,若没有定义
toString
方法,则输出内部属性[[Class]]
其他几条规则都比较明确,让我们来重点关注下第 6 条。JS 中除了普通对象之外,大多数都定义了自己的toString
方法,比如Function.prototype.toString
会输出函数源码,Array.prototype.toString
会把值转换成字符串后在每个值中间插入逗号输出。这时候要获取[[Class]]
属性的操作就要借助Object.prototype.toString
来实现了,也就是我们非常熟悉的Object.prototype.toString.call(obj)
操作了。
ToNumber
ToNumber 操作会将其他类型的值转换成数字类型,该操作具有以下规则
- true -> 1
- false -> 0
- undefined -> NaN
- null -> 0
- 字符串的转换遵循通用规则,但以 0 开头的八进制数无法正确识别。注意,空字符串转换后为 0
- 对于其他对象,会对其进行 ToPrimitive 抽象操作,期待得到基本类型的值,然后对该基本类型的值进行 ToNumber 操作
这里有一个特殊的地方要注意,undefined
会被转换成NaN
,而null
则会被转换成0
。另外ToPrimitive
具体对应什么样的操作呢?
ToPrimitive
ToPrimitive 操作会将其他类型的值转换成基础类型的值,该操作具有以下规则
- 调用对象的 valueOf()方法,如果返回基本类型值,则返回该值,否则继续
- 调用对象的 toString()方法,如果返回基本类型值,则返回该值,否则抛出 TypeError 异常
除非特意实现过,否则大部分对象的 valueOf 方法会返回自身,所以实际环境中,ToPrimitive 操作大部分时候是依赖 toString 方法完成的。注意,刚刚说的只是实践经验,具体操作时必须严格按照规范来。
ToBoolean
ToBoolean 操作会将其他类型的值转换成布尔类型值。首先 ES 规范中先规定了一个假值列表
- undefined
- null
- false
- ±0/NaN
""
于是 ToBoolean 操作的规则是这样的
- 假值列表中的值 -> false
- 其他值 -> true
很简单对吧,不在假值列表中的值就是 true。
宽松相等
说起宽松相等,大部分开发者对它深恶痛绝,因为有太多反直觉的结果,所以大部分代码规范都要求开发者只使用严格相等,不要使用宽松相等。甚至有开发者整理了 JS 中各种典型值的相等关系表,可谓非常壮观。
但我觉得逃避解决不了问题,知其所以然才能有效避坑,所以让我们来探究一下宽松相等的原理。
首先,宽松相等和严格相等的区别是,宽松相等允许对操作符两侧的值按照“一定规则”进行类型转换。注意这里的“类型转换”,看起来它就是“万恶之源”了。让我们一起看看刚刚提到的“一定规则”具体是什么样的
- 运算符两侧分别是字符串和数字时,对字符串进行 ToNumber 操作
- 运算符两侧分别是布尔值和其他类型值时,对布尔值进行 ToNumber 操作
- 运算符两侧分别是 null 和 undefined 时,返回 true
- 运算符两侧分别是对象和数字/字符串时,对对象进行 ToPrimitive 操作
了解完规则之后,我们来拿文章最开始的例子检验一下。就拿![] == []
来说吧,它的结果为 true,很反直觉对吧,空数组居然和它的取反相等,让我们来看一下 true 是被怎么推导出来的。
- 对符号左侧表达式
![]
求值,由于[]
不在假值列表里,所以![]
的结果为 false false == []
按照规则,需要对 false 进行 ToNumber 操作,结果为 00 == []
一侧是对象,一侧是数字,所以对对象进行 ToPrimitive 操作[].valueOf()
返回结果是[]
,不符合要求[].toString()
返回结果是""
,符合要求0 == ""
按照规则,对""
进行 ToNumber 操作,结果是 00 == 0
结果为 true
怎么样,对照规则一步步走下来,逻辑是不是清晰多了?
逻辑比较
所谓逻辑比较就是大于小于这些,逻辑比较中也是存在类型转换的。其转换规则如下
- 如果双方不都是字符串
- 对双方进行 ToPrimitive 操作
- 如果结果依然存在非字符串, 则对结果再次进行 ToNumber 操作,比较数字大小
- 如果结果均为字符串,则按字符串情况比较
- 双方都是字符串,则按字母顺序比较
值得注意的一点是,大于等于和小于等于是通过逻辑取反得到结果的,也就说(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 这两种情况,很实用,也很安全。希望大家看了这篇文章之后能摘掉有色眼镜来看待==
运算符,并对它加以合理利用。