if (!areArrays) {
if (typeof a != 'object' || typeof b != 'object') return false;
var aCtor = a.constructor, bCtor = b.constructor;
if (aCtor !== bCtor && !(_.isFunction(aCtor) && aCtor instanceof aCtor &&
_.isFunction(bCtor) && bCtor instanceof bCtor)
&& ('constructor' in a && 'constructor' in b)) {
return false;
}
}
underscore的源码基本上都是由各种短小精悍的函数组成,每个函数有自己的功能,一些较高级的功能会调用其他的函数作为自己的工具函数以达到逻辑的复用,同时也缩短方法的篇幅。但
eq
方法是个例外,它调用的工具方法也不多,但却是整个underscore中最长的方法,究其原因,是因为这个方法太万能了,它能比较任意两个对象是否相等,不论这个对象是什么类型。根据
eq
方法的比较逻辑,也能归纳出JS世界中的一些规律:变量类型
JS中的数据类型分为两类:原始类型和对象类型。其中原始类型包括:数字
Number
,字符串String
和布尔值Boolean
。对象类型包括普通对象Object
,数组Array
,函数Function
等。每种类型都有特别的比较方法。eq
方法通过逐步排除,由简单到复杂的顺序完成了相等数据的判断。函数结构
整个函数体的结构,即判断的逻辑顺序如下:
把简单的原始类型比较的逻辑放在前面是因为后面的对象类型比较逻辑中存在自身的递归调用,会将包裹一层层剥除,子元素可能是原始类型,所以原始类型的比较要放在前面。
判断逻辑
直接调用
===
严格相等运算符是最简单的,首先调用它。它可以方便地直接对原始类型数据进行比较,如果结果为
true
,那么99%可以确定两个元素是相等的。那1%的不确定是在于
0
和-0
,它们可以通过===
的检测,但不应该把它们看做相等,因为它们在JS的数学运算中会表现出不同的性质。通过如下语句排除掉0
和-0
的相等性:另外,还有一些特殊情况需要进一步检测,首先是
null
和undefined
。将它们判断掉也可以避免影响后面的判断,由于它们能通过==
的判断,所以要用===
严格区分:underscore可能会用
_
对象将变量包裹起来,如果是这种情况需要把被包裹的值提取出来才可以进行下一步判断:根据
[[Class]]
判断下面则是原始数据类型判断的最后一步,根据
[[Class]]
值进行判断,这种判断能覆盖原始数据类型未被覆盖到的所有剩余情况,首先取得a的[[Class]]
并和b的[[Class]]
先比较一下:然后通过一个
switch (className)
语句进行分类判断:[object RegExp]和[object String]:
它们的判断方式一样,统一转换为字符串后进行严格相等的判断:
[object Number]:
先考虑特殊情况
NaN
,a和b都是NaN
时它们会不等,但应该把它们看做相等的,因为NaN
总是表现出一样的性质,解决办法是判断a和b是否分别为NaN
。最后再判断一次相等性,同时剔除-0
的情况,这和eq
方法刚开始的逻辑似乎重复了,不知道是不是:[object Date]和[object Boolean]:
它们的判断很简单,直接调用
===
:对象数据类型判断
原始数据类型的所有判断已经结束,下面就是对象数据类型,即纯粹对象和数组类型的判断。它们_基本上_就是原始数据类型一种组合,因此对它们的判断实际上是将它们逐步分解成原始数据类型,然后递归调用
eq
。针对数组和对象的分解逻辑是不一样的,所以首先要判断a和b是数组还是对象:
后面会根据
areArrays
的真假走不同的逻辑分支。但在此之前,为了简化判断,先要排除一种情况,那就是如果是对象的话,可以先比较它们的构造函数,构造函数不同的话,即使对象内的值相同,两个对象也是不同的:通过构造函数的比较后,即进入具体包含值的比较,后面紧跟的while循环在第一次遍历的时候是不会执行的。后面将a和b分别压入堆栈,堆栈的作用是按照顺序存放比较对象的元素值,并递归调用eq方法自身。对于a或者b来说,如果某个子元素仍然是对象或者数组,则会将这个子元素继续拆分,直到全部拆分为eq方法前半部分所写的,可以比较的“基本单元”为止,一旦有任何一个元素不相等,便会触发一连串的
return false
。至于数组和对象的区别并不是太重要,underscore本身提供的工具函数可以处理数据结构上的差异性,本质还是eq方法本身。