数组去重是一个老生常谈的问题。平常的处理可能只是对仅包含简单数据类型的数组进行操作,今天我们对复合数据类型做一个讨论。
要实现数组去重,我们首先要有一个比较两个数据是否相等的函数。Underscore.js给我们提供了一个很好的功能函数,名为:_.isEqual()。在对复合数据类型的比较上,它在实现上引入的两个类似于堆栈(后进后出)的数组(只调用了其push()和pop()方法实现堆栈),并采用递归的方式嵌套判断。其源码如下:
// eq函数只在isEqual方法中调用, 用于比较两个数据的值是否相等
// 与 === 不同在于, eq更关注数据的值
// 如果进行比较的是两个复合数据类型, 不仅仅比较是否来自同一个引用, 且会进行深层比较(对两个对象的结构和数据进行比较)
var eq = function(a, b, aStack, bStack) {
// 检查两个简单数据类型的值是否相等
// 对于复合数据类型, 如果它们来自同一个引用, 则认为其相等
// 如果被比较的值其中包含0, 则检查另一个值是否为-0, 因为 0 === -0 是成立的
// 而 1 / 0 == 1 / -0 是不成立的(1 / 0值为Infinity, 1 / -0值为-Infinity, 而Infinity不等于-Infinity)
if (a === b) return a !== 0 || 1 / a == 1 / b;
// 将数据转换为布尔类型后如果值为false, 将判断两个值的数据类型是否相等(因为null与undefined在非严格比较下值是相等的)
if (a == null || b == null) return a === b;
// 如果进行比较的数据是一个Underscore封装的对象(通过判断比较对象是否是_函数的原型)
// 则将对象解封后获取本身的数据(通过_wrapped访问), 然后再对本身的数据进行比较
// 它们的关系类似与一个jQuery封装的DOM对象, 和浏览器本身创建的DOM对象
// 以上的比较基本解决所有简单数据类型的值的比较问题,对于复合数据类型会先通过比较Object.prototype.toString()方法在比较对象上作用后的结果,若不相等直接返回false,否则做进一步判断
if (a instanceof _) a = a._wrapped;
if (b instanceof _) b = b._wrapped;
var className = toString.call(a);
// 不相等说明数据类型不一致,直接返回false
if (className != toString.call(b)) return false;
switch (className) {
// 字符串,数字,日期和布尔值通过值来比较
case '[object String]':
// 原始值和它们对应的封装对象调用toString()方法后的结果是一样的,如下:
// Object.prototype.toString.call(new String('5'))-->"[object String]"
// Object.prototype.toString.call('5')-->"[object String]"
return a == String(b);
case '[object Number]':
// 通过+a将a转成一个Number, 如果a被转换之前与转换之后不相等, 则认为a是一个NaN类型
// 因为NaN与NaN是不相等的, 因此当a值为NaN时, 无法简单地使用a == b进行匹配, 而是用相同的方法检查b是否为NaN(即 b != +b)
// 当a值是一个非NaN的数据时, 则检查a是否为0, 因为当b为-0时, 0 === -0是成立的(实际上它们在逻辑上属于两个不同的数据)
return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b);
// 对日期类型没有使用return或break, 因此会继续执行到下一步(无论数据类型是否为Boolean类型, 因为下一步将对Boolean类型进行检查)
case '[object Date]':
case '[object Boolean]':
// Coerce dates and booleans to numeric primitive values. Dates are compared by their
// millisecond representations. Note that invalid dates with millisecond representations
// of `NaN` are not equivalent.
// 将日期或布尔类型转换为数字
// 日期类型将转换为数值类型的时间戳(无效的日期格式将被换转为NaN,从而无效的日期格式被认为是不相等的)
// 布尔类型中, true被转换为1, false被转换为0
// 比较两个日期或布尔类型被转换为数字后是否相等
return +a == +b;
// RegExps are compared by their source patterns and flags.
// 正则表达式类型, 通过source访问表达式的字符串形式
// 检查两个表达式的字符串形式是否相等
// 检查两个表达式的全局属性是否相同(包括g, i, m)
// 如果完全相等, 则认为两个数据相等
case '[object RegExp]':
return a.source == b.source &&
a.global == b.global &&
a.multiline == b.multiline &&
a.ignoreCase == b.ignoreCase;
break;
// 这里是我自己加的一个判断,因为在Underscore原来的代码一下结果为true:
// _.isEqual({a:1},{a:1});// true
// 而对以下结果则判定为false:
// _.isEqual({a:function(){}},{a:function(){}}); // false
// 我觉得不甚合理(如果不对请指出),故完善了此处的判断,让上面的结果为true
case '[object Function]':
var repReg = /\r|\n|\t|\v|\s*/g;
return a.toString().replace(repReg,'') === b.toString().replace(repReg,'');
}
// 当执行到此时, ab两个数据应该为类型相同的对象或数组类型
if (typeof a != 'object' || typeof b != 'object') return false;
// 假设循环结构是相等的。检测循环结构相等的算法被ES标准15.12.3部分所采纳(这是在赤果果的炫耀吗),将操作抽象为'JO'
var length = aStack.length;
while (length--) {
// 线性搜索。性能是跟某一处的嵌套结构的数量成反比的。
// 其实此处就是对一个复合数据类型全部递归判断完成后的判断条件。而该复合数据类型也可能是另一个复合数据类型的一部分
if (aStack[length] == a) return bStack[length] == b;
}
// 对象的构造函数如果不同则认为是不想等的,但是Object instanceof Object为true,Function instanceof Function为true,认为他们是相等的
var aCtor = a.constructor, bCtor = b.constructor;
if (aCtor !== bCtor && !(_.isFunction(aCtor) && (aCtor instanceof aCtor) &&
_.isFunction(bCtor) && (bCtor instanceof bCtor))) {
return false;
}
// Add the first object to the stack of traversed objects.
// 将对比对象加入到堆栈已进行递归遍历
aStack.push(a);
bStack.push(b);
var size = 0, result = true;
// Recursively compare objects and arrays.
// 递归的进行对象和数组的复合数据类型
if (className == '[object Array]') {
// Compare array lengths to determine if a deep comparison is necessary.
// size记录数组的长度,长度不相等直接返回false
size = a.length;
result = size == b.length;
if (result) {
// Deep compare the contents, ignoring non-numeric properties.
// 递归进行检测,忽略非数值的属性(非数值属性不会进行到这)
while (size--) {
if (!(result = eq(a[size], b[size], aStack, bStack))) break;
}
}
} else {
// Deep compare objects.
// 深层比较对象
for (var key in a) {
if (_.has(a, key)) {
// Count the expected number of properties.
// 记录属性的个数
size++;
// Deep compare each member.
// 深层次递归遍历比较结果
if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break;
}
}
// Ensure that both objects contain the same number of properties.
// 如果对某一层的符合数据类型比较结果为true,因为如果b对象包含a的所有属性且值相等而b包含a不包含的属性,此时也会返回true。骨堆它们的属性数量进行比较(包含原型链上的属性)
if (result) {
for (key in b) {
if (_.has(b, key) && !(size--)) break;
}
result = !size;
}
}
// Remove the first object from the stack of traversed objects.
// 根据递归往“堆栈”里push内容的顺序,将已经进行过比较的内容出栈。
aStack.pop();
bStack.pop();
return result;
};
// Perform a deep comparison to check if two objects are equal.
_.isEqual = function(a, b) {
return eq(a, b, [], []);
};
function removeRepeat(ary){
var len = ary.length;
for(var i=0;i