阅读 3184

我对 JS 中相等和全等操作符转化过程一直很迷惑,直到有了这份算法

作者:Dmitri Pavlutin

译者:前端小智

来源:dmitripavlutin


阿里云最近在做活动,低至2折,有兴趣可以看看: promotion.aliyun.com/ntms/yunpar…


在日常的 JS 编码过程中,可能很难看到相等运算符(==)是如何工作的。特别是当操作数具有不同类型时。这有时会在条件语句中产生一些难以识别的 bug。很容易理解为什么 0 == 8flase 的或者 '' == falsetrue。但是为什么{} == truefalse 的就看不出来了。接下将会讲这是肿么肥事。

在这之前,先说几个术语:

  • 操作符(Operator) 表示操作的符号。例如,相等运算符==比较两个值,三等运算符 === 比较两个值及其类型,加法运算符+两个数字和或连接两个字符串。

  • 操作数(Operand) 是运算的主体,是执行运算的数量。例如,在表达式 0 == {} 中,0 是第一个操作数,{} 是第二个操作数。

  • JS 中的基本数据类型(原始类型)有 numberstring, booleannullundefinedsymbol

全等运算符 ===

全等和不全等操作符遵循以下基本规则(IEA规则):

  1. 如果两个操作数有不同的类型,它们不是严格相等的
  2. 如果两个操作数都为 null,则它们是严格相等的
  3. 如果两个操作数都为 undefined,它们是严格相等的
  4. 如果一个或两个操作数都是 NaN,它们就不是严格相等的
  5. 如果两个操作数都为 true 或都为 false,它们是严格相等的
  6. 如果两个操作数都是 number 类型并且具有相同的值,则它们是严格相等的
  7. 如果两个操作数都是 string 类型并且具有相同的值,则它们是严格相等的
  8. 如果两个操作数都引用相同的对象或函数,则它们是严格相等的
  9. 以上所有其他情况下操作数都不是严格相等的。

规则很简单。

值得一提的是,在全等运算中,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
复制代码

两个变量 firstObjectsecondObject 都是对同一对象的引用,根据 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 源代码字符串。

日期 DatetoString() 方法返回了一个可读的日期和时间字符串。

RegExptoString() 方法将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() 方法只是简单返回对象本身。日期 DatevalueOf() 方法会返回它的一个内部表示:1970年1月1日以来的毫秒数。

new Date(2019,9,16).valueOf() // 1571155200000
复制代码

通过使用 toString()valueOf() 方法,就可以做到对象到字符串和对象到数字的转换了。但需要注意的是,在某些特殊的场景中,JS 执行了完全不同的对象到原始值的转换。

JS 中对象到字符串的转换经过如下这些步骤,咱们简称 OPCA 算法。

  1. 如果方法 valueOf() 存在,则调用它。如果 valueOf() 返回一个原始值,JS 将这个值转换为字符串(如果本身不是字符串的话),并返回这个字符串结果。

  2. 如果方法 toString() 存在,则调用它。如果 toString() 返回一个原始值,JS 将这个值转换为字符串(如果本身不是字符串的话),并返回这个字符串结果。需要注意,原始值到字符串的转换。

  3. 否则,JS 无法从 toString()valueOf() 获得一个原始值,它将抛出一个 TypeError:不能将对象转换为原始值 异常

当调用 valueOf() 方法时,大多数原生对象都会返回对象本身。因此 toString() 方法使用得更频繁。

关于 Date 对象的注意事项:在转换为原始值时,对象立即使用 toString() 方法转换为字符串。这样,规则1就被跳过了。普通的 JS 对象,{}new object(),通常被转换成 "[object Object]"

数组通过将它的元素与“,”分隔符连接转换为。例如 [1,3,"four"] 被转换成" 1,3,four"

相等运算符 ==

相等运算符 “==” 如果两个操作数不是同一类型,那么相等运算符会尝试一些类型转换,然后进行比较。

相等运算符算法(EEA)

  1. 如果操作数具有相同的类型,请使用上面的 IEA 测试它们是否严格相等。 如果它们不严格相等,则它们不相等,否则相等。
  2. 如果操作数有不同的类型:
    2.1) 如果一个操作数为 null 而另一个 undefined,则它们相等
    2.2) 如果一个值是数字,另一个是字符串,先将字符串转换为数字,然后使用转换后的值比较
    2.3) 如果一个操作数是布尔值,则将 true 转换为 1,将 false 转换为 0,然后使用转换后的值比较
    2.4) 如果一个操作数是一个对象,而另一个操作数是一个数字或字符串,则使用OPCA将该对象转换为原原始值,再使用转换后的值比较
  3. 在以上的其他情况下,操作数都不相等

例 1

1 == true // true
复制代码

上面的转换步骤:

  1. 1 == true (使用EEA 规则2.3 将 true 转换为 1)
  2. 1 == 1(操作数有相同的类型。使用 EEA 规则1 将相等转换为全等运算进行比较
  3. 1 === 1(两个操作数都是数字,并且具有相同的值。根据 IEA 规则 6,这是相等的)
  4. true

例 2

'' == 0 // true
复制代码

上面的转换步骤:

  1. '' == 0(一个操作数是字符串,另一个操作数是数字,根据EEA规则2.2'' 被转换为数字 0 )
  2. 0 == 0(操作数类型相同,使用 EEA规则1 将相等转换为全等运算进行比较)
  3. 0 === 0(操作数类型相同,值相同,所以根据IEA规则6,它是一个恒等式)
  4. true

例 3

null == 0 // false
复制代码

上面的转换步骤:

  1. null == 0 (null 是原始类型,0 是 number 类型。根据EEA规则3)
  2. false

例 4

null == undefined // true
复制代码

上面的转换步骤:

  1. null == undefined(基于EEA规则2.1,操作数相等)
  2. true

例 5

NaN == NaN // false
复制代码

上面的转换步骤:

  1. NaN == NaN(两个操作数都是数字。根据EEA规则1,将相等转换为全等运算进行比较)
  2. NaN === NaN(根据IEA规则4,操作数严格不相等)
  3. false

例 6

[''] == '' // true
复制代码

上面的转换步骤:

  1. [''] == ''(['']是一个数组和 '' 是一个字符串。应用EEA规则2.4并使用OPCA规则2将数组转换为原始值 '')
  2. '' == '' (两个操作数都是字符串,将相等转换为全等运算进行比较)
  3. '' === '' (两个操作数类型相同,值相同。使用IEA规则7,它们是相等的)
  4. true

例 7

{} == true // false
复制代码

上面的转换步骤:

  1. {} == true(使用EEA规则2.3,将 true 操作数转换为 1)
  2. {} == 1(第一个操作数是一个对象,因此有必要使用OPCA将其转换为原始值)
  3. “[object object]”== 1(因为第一个操作数是字符串,第二个操作数是数字,根据 EEA规则2.2“[object object]”转换为数字)
  4. NaN == 1(两个操作数都是数字,因此使用 EEA规则1 将相等转换为全等运算进行比较)
  5. NaN === 1(根据 IEA规则4,没有什么是与 NaN 相等的,结果是 false)
  6. false

实用技巧

即使在详细研究了本文中的所有示例、学习了算法之后,你会发现要立即理解复杂的比较还需要时间的积累。

告诉你一些技巧。 将本文添加到书签中(使用Ctrl + D),下一次看到有趣的情况时,可以根据等式算法编写逐步的计算。 如果检查至少 10 个示例,则以后不会有任何问题。

现在就可以试试,如 [0] == 0 的结果和转化步骤是什么?

相等运算符==进行类型转换。因此,可能会产生意想不到的结果,例如 {}== truefalse( 参见例7)。在大多数情况下,使用全等操作符 === 更安全。

总结

相等和全等运算符号可能是最常用的运算符之一。理解它们是编写稳定且bug较少的 JS 的步骤之一。

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

原文:dmitripavlutin.com/the-legend-…

交流(欢迎加入群,群工作日都会发红包,互动讨论技术)

阿里云最近在做活动,低至2折,有兴趣可以看看:promotion.aliyun.com/ntms/yunpar…

干货系列文章汇总如下,觉得不错点个Star,欢迎 加群 互相学习。

github.com/qq449245884…

因为篇幅的限制,今天的分享只到这里。如果大家想了解更多的内容的话,可以去扫一扫每篇文章最下面的二维码,然后关注咱们的微信公众号,了解更多的资讯和有价值的内容。

clipboard.png

每次整理文章,一般都到2点才睡觉,一周4次左右,挺苦的,还望支持,给点鼓励

关注下面的标签,发现更多相似文章
评论