作者:Dmitri Pavlutin
译者:前端小智
来源:dmitripavlutin
阿里云最近在做活动,低至 2 折,有兴趣可以看看:
https://promotion.aliyun.com/…
在日常的 JS 编码过程中,可能很难看到相等运算符 (=
) 是如何工作的。特别是当操作数具有不同类型时。这有时会在条件语句中产生一些难以识别的 bug。很容易理解为什么 0 == 8
是 flase
的或者 '' == false
是 true
。但是为什么 {} == true
是 false
的就看不出来了。接下将会讲这是肿么肥事。
在这之前,先说几个术语:
-
操作符 (Operator) 表示操作的符号。例如,相等运算符
==
比较两个值,三等运算符===
比较两个值及其类型,加法运算符+
两个数字和或连接两个字符串。 -
操作数(Operand) 是运算的主体,是执行运算的数量。例如,在表达式
0 == {}
中,0
是第一个操作数,{}
是第二个操作数。 - JS 中的基本数据类型 (原始类型) 有
number
,string
,boolean
,null
和undefined
,symbol
。
全等运算符 ===
全等和不全等操作符遵循以下基本规则(IEA 规则):
- 如果两个操作数有不同的类型,它们不是严格相等的
- 如果两个操作数都为
null
,则它们是严格相等的 - 如果两个操作数都为
undefined
,它们是严格相等的 - 如果一个或两个操作数都是
NaN
,它们就不是严格相等的 - 如果两个操作数都为
true
或都为false
,它们是严格相等的 - 如果两个操作数都是
number
类型并且具有相同的值,则它们是严格相等的 - 如果两个操作数都是
string
类型并且具有相同的值,则它们是严格相等的 - 如果两个操作数都引用相同的对象或函数,则它们是严格相等的
- 以下所有其他情况下操作数都不是严格相等的。
规则很简单。
值得一提的是,在全等运算中,NaN
与其他任何值相比, 结果都是 false
。来看看考虑些例子,这是学习这些规则的好方式。
例 1
1 === "1" // false, 规则 1
操作数是不同的类型(数字和字符串),基于 IEA 规则 1,它们是不等的。
例 2
0 === 0 // true, 规则 6
操作数具有相同的类型和相同的值,因此根据 IEA 规则 6,它们是严格相等的。
例 3
undefined === undefined // true, 规则 3
两个操作数都是 undefined
的,应用 IEA 规则 3,它们是相等的。
例 4
undefined === null // false, 规则 1
因为操作数是不同的类型,根据 IEA 规则 1,它们并不相同。
例 5
NaN === NaN // false, IEA 规则 5
操作数是相同的类型,但是 IEA 规则 4 表明任何与 NaN 比较都是不相等的。
例 6
var firstObject = {},
secondObject = firstObject;
secondObject['name'] = 'Neo';
secondObject === firstObject // true, IEA 规则 8
两个变量 firstObject
和 secondObject
都是对同一对象的引用,根据 IEA 规则 8,它们相等。
例 7
[] === [] //false, IEA 规则 9
字面量 []
创建了一个新的数组引用。这两个操作数是相同的类型(对象),但是它们引用不同的对象。根据 IEA 规则 9 , 它们不相等。
对象转换为原始值的规则
对象到布尔值
对象到布尔值的转换非常简单:所有的对象(包括数字和函数)都转换为 true
。对于包装对象亦是如此:new Boolean(false)
是一个对象而不是原始值,它将转换为 true
。
对象到字符串
对象到字符串 和 对象到数字 的转换都是通过调用待转换对象的一个方法来完成的。一个麻烦的事实是,JS 对象有两个不同的方法来执行转换,接下来要讨论的一些特殊场景更加复杂。值得注意的是,这里提到的字符串和对象的转换规则只适用于原生对象(native object)。宿主对象(例如有 Web 浏览器定义的对象)根据各自的算法可以转换成字符串和数字。
所有的对象继承了两个转换方法。第一个是toString()
,它的作用是返回一个反映这个对象的字符串。默认的 toString()
方法并不会返回一个有趣的值:
({x:1,y:2}).toString() //=>"[object object]"
很多类定义了更多特定版本的 toString()
方法。例如,数组的 toString()
方法是将每个数组元素转换为一个字符串,并在元素之间添加逗号后合并成结果字符串。
函数的 toString()
方法返回了这个函数的实现定义。实际上,这里的实现是通常是将用户定义的函数转换为 JS 源代码字符串。
日期 Date
的 toString()
方法返回了一个可读的日期和时间字符串。
RegExp
的 toString()
方法将 RegExp 对象转换为表示正则表达式直接量的字符串:
来几个例子:
[1,2,3].toString() //=> "1,2,3"
(function(x){f(x); }).toString() // => "function(x){f(x); }"
/\d+/g.toString() // => "/\d+/g"
new Date(2019,9,16).toString() //=> "Wed Oct 16 2019 00:00:00 GMT+0800 (中国标准时间)"
另一个转换对象的函数是 valueOf()
。如果存在任意原始值,它就默认将对象转换为表示它的原始值。对象是复合值,而且大多数对象无法真正表示为一个原始值,因此默认的 valueOf()
方法简单地返回对象本身,而不是返回一个原始值。数组、函数和正则表达式简单地继承了这个方法,调用这些类型的实例的valueOf()
方法只是简单返回对象本身。日期 Date
的 valueOf()
方法会返回它的一个内部表示:1970 年 1 月 1 日以来的毫秒数。
new Date(2019,9,16).valueOf() // 1571155200000
通过使用 toString()
和 valueOf()
方法,就可以做到对象到字符串和对象到数字的转换了。但需要注意的是,在某些特殊的场景中,JS 执行了完全不同的对象到原始值的转换。
JS 中对象到字符串的转换经过如下这些步骤,咱们简称 OPCA 算法。
- 如果方法
valueOf()
存在,则调用它。如果valueOf()
返回一个原始值,JS 将这个值转换为字符串(如果本身不是字符串的话),并返回这个字符串结果。 - 如果方法
toString()
存在,则调用它。如果toString()
返回一个原始值,JS 将这个值转换为字符串(如果本身不是字符串的话),并返回这个字符串结果。需要注意,原始值到字符串的转换。 - 否则,JS 无法从
toString()
或valueOf()
获得一个原始值, 它将抛出一个TypeError: 不能将对象转换为原始值
异常
当调用 valueOf()
方法时,大多数原生对象都会返回对象本身。因此 toString()
方法使用得更频繁。
关于 Date
对象的注意事项: 在转换为原始值时,对象立即使用 toString()
方法转换为字符串。这样,规则 1 就被跳过了。普通的 JS 对象,{}
或 new object()
,通常被转换成 "[object Object]"
数组通过将它的元素与 “,”
分隔符连接转换为。例如 [1,3,"four"]
被转换成"1,3,four"
。
相等运算符 ==
相等运算符 “==”
如果两个操作数不是同一类型,那么相等运算符会尝试一些类型转换,然后进行比较。
相等运算符算法(EEA)
- 如果操作数具有相同的类型,请使用上面的 IEA 测试它们是否严格相等。如果它们不严格相等,则它们不相等,否则相等。
- 如果操作数有不同的类型:
2.1 如果一个操作数为null
而另一个undefined
,则它们相等
2.2 如果一个值是数字,另一个是字符串,先将字符串转换为数字,然后使用转换后的值比较
2.3 如果一个操作数是布尔值,则将true
转换为1
,将false
转换为0
,然后使用转换后的值比较
2.4 如果一个操作数是一个对象,而另一个操作数是一个数字或字符串,则使用OPCA 将该对象转换为原原始值,再使用转换后的值比较 - 在以上的其他情况下,操作数都不相等
例 1
1 == true // true
上面的转换步骤:
-
1 == true
(使用 EEA 规则 2.3 将true
转换为1
) -
1 == 1
(操作数有相同的类型。使用 EEA 规则 1 将相等转换为全等运算进行比较 -
1 === 1
(两个操作数都是数字,并且具有相同的值。根据 IEA 规则 6,这是相等的) true
例 2
'' == 0 // true
上面的转换步骤:
-
''== 0
(一个操作数是字符串,另一个操作数是数字,根据EEA 规则 2.2,''
被转换为数字0
) - 0 == 0(操作数类型相同,使用 EEA 规则 1 将相等转换为全等运算进行比较)
-
0 === 0
(操作数类型相同,值相同,所以根据IEA 规则 6 ,它是一个恒等式) true
例 3
null == 0 // false
上面的转换步骤:
-
null == 0
(null
是原始类型,0 是number
类型。根据EEA 规则 3 ) false
例 4
null == undefined // true
上面的转换步骤:
-
null == undefined
(基于EEA 规则 2.1,操作数相等) true
例 5
NaN == NaN // false
上面的转换步骤:
-
NaN == NaN
(两个操作数都是数字。根据EEA 规则 1 ,将相等转换为全等运算进行比较) -
NaN === NaN
(根据IEA 规则 4 ,操作数严格不相等) false
例 6
[''] =='' // true
上面的转换步骤:
-
[''] ==''
(['']
是一个数组和''
是一个字符串。应用 EEA 规则 2.4 并使用OPCA 规则 2
将数组转换为原始值''
) -
''==''
(两个操作数都是字符串,将相等转换为全等运算进行比较) -
''===''
(两个操作数类型相同,值相同。使用IEA 规则 7 ,它们是相等的) true
例 7
{} == true // false
上面的转换步骤:
-
{} == true
(使用EEA 规则 2.3,将true
操作数转换为1
) -
{} == 1
(第一个操作数是一个对象,因此有必要使用 OPCA 将其转换为原始值) -
“[object object]”== 1
(因为第一个操作数是字符串,第二个操作数是数字,根据 EEA 规则 2.2 将“[object object]”
转换为数字) -
NaN == 1
(两个操作数都是数字,因此使用 EEA 规则 1 将相等转换为全等运算进行比较) -
NaN === 1
(根据 IEA 规则 4 ,没有什么是与NaN
相等的,结果是false
) false
实用技巧
即使在详细研究了本文中的所有示例、学习了算法之后,你会发现要立即理解复杂的比较还需要时间的积累。
告诉你一些技巧。将本文添加到书签中(使用Ctrl + D),下一次看到有趣的情况时,可以根据等式算法编写逐步的计算。如果检查至少 10
个示例,则以后不会有任何问题。
现在就可以试试,如 [0] == 0
的结果和转化步骤是什么?
相等运算符 ==
进行类型转换。因此,可能会产生意想不到的结果,例如 {}== true
是 false
(参见例 7)。在大多数情况下,使用全等操作符 ===
更安全。
总结
相等和全等运算符号可能是最常用的运算符之一。理解它们是编写稳定且 bug 较少的 JS 的步骤之一。
代码部署后可能存在的 BUG 没法实时知道,事后为了解决这些 BUG,花了大量的时间进行 log 调试,这边顺便给大家推荐一个好用的 BUG 监控工具 Fundebug。
原文:https://dmitripavlutin.com/th…
交流
阿里云最近在做活动,低至 2 折,有兴趣可以看看:https://promotion.aliyun.com/…
干货系列文章汇总如下,觉得不错点个 Star,欢迎 加群 互相学习。
https://github.com/qq449245884/xiaozhi
因为篇幅的限制,今天的分享只到这里。如果大家想了解更多的内容的话,可以去扫一扫每篇文章最下面的二维码,然后关注咱们的微信公众号,了解更多的资讯和有价值的内容。
每次整理文章,一般都到 2 点才睡觉,一周 4 次左右,挺苦的,还望支持,给点鼓励