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