数组去重多种方法

229 阅读5分钟

去重,老生常谈了。前几年面试的时候,不问一下,好像都对不起面试者那样。以前我也被问,用es6的Set方法解答,或hash方法,觉得答案还可以让面试官满意。但,随着头发掉得越来越多,也就知道,其实去重也不简单。

为啥呢,因为去重,你首先得判断,数据是否重复了,也就是是否存在数据相等,根据之前的学习 判断数据是否相等(undescore eq 源码) ,可知数据相等,是存在很多种情况,以前的方法,不太够用了。当然,这是为了学习才折腾的,日常工作中,Set方法应该是够用的了。

首先看一下 hash 方法去重

var arr = [1, '1', 2, 3, 4, +0, -0, true, 'true'];
function unique(arr) {
    var hash = {};
    var result = [];
    var item;
    for (var i = 0, l = arr.length; i < l; i++) {
        item = arr[i];
        if (!hash[item]) {
            hash[item] = true;
            result.push(item);
        }
    }
    return result;
}
// [ 1, 2, 3, 4, 0, true]
unique(arr);

从结果上看,hash方法去重,无法区分 字符串跟非字符串,因为对象的key类型是字符串,所以 1 也会转成 '1' 来处理。

来看下 Set 方法去重

var arr = [1, '1', 2, 3, 4, +0, -0, true, 'true'];
function unique(arr) {
    return Array.from(new Set([...arr]));
}
// [ 1, '1', 2, 3, 4, 0, true, 'true']
unique(arr);

这就将 hash 方法的缺陷给弥补了,可以说,除了兼容问题外,日常工作中使用是没有很大问题的。

接下来,我们开始探讨一些问题:

1 跟 new Number(1) 是否应该等同?

实践中,new Number(1) 我们应该是想得到它的值,也就是去重时,new Number(1) 应等同于 1。

而,这个方面,hash方法跟Set方法,表现又不一样了

var arr = [1, '1', 2, 3, 4, +0, -0, true, 'true', new Number(1)];
// hash 结果是 [ 1, 2, 3, 4, 0, true]
// Set 结果是 [ 1, '1', 2, 3, 4, 0, true, 'true', [Number: 1]]

因为hash方法在 hash[item] 的时候,调用了 toString 方法,因此 new Number(1) 变成了 '1'。这可以视为是一个错误了。 Set 方法本来就区分类型,所以表现中规中矩,也没达到我们想要的 new Number(1) 视为 1的结果。

所以,在去重之前,我们可以增加对应的类型对象转化为对应值的操作更改hash中key存在的判断依据(也就是 hash[item]),就可以达到我们想要的效果了。

当然,这是在吹毛求疵,大部分需要去重的数组子项都是数字,字符串类型的。

接下来,你以为是结束了?

too native!

如果我们遇到了对象数组呢,这个还是比较常见的,那么hash方法跟Set方法表现如何呢?

var arr = [{a: 1}, new Object({a: 1}), {b: 21}];
// hash 结果是 [ { a: 1 } ]
// Set 结果是 [ { a: 1 }, { a: 1 }, { b: 21 } ]

hash方法再现错误!Set仍然是循规蹈矩。

hash方法之所以出现这个结果,是因为 在 hash[item] 的时候,调用了 toString 方法,也就是 {a: 1} 被处理为 new Object({a: 1}).toString() 也就是 "[object Object]",接下来其他对象也被转化,而hash["[object Object]"] 在处理第一个的时候就存在了,所以会导致,结果只返回了第一个对象。

Set 中 new Object 均会视为一个新的 object,所以它认为这是一个不同的新值。

接下来,你可能习惯了,因为问题还没结束!

二维数组,闪亮登场!接着看它们表现

var arr = [[1,2], new Array(1,2), [2,3]];
// hash 结果是 [ [ 1, 2 ], [ 2, 3 ] ]
// Set 结果是 [ [ 1, 2 ], [ 1, 2 ], [ 2, 3 ] ]

意外!hash方法竟然不犯错,返回了正确的结果。而 Set 仍然是爱活不活的状态。

其实是因为 new Array(1,2).toString() 的结果是 "1,2",返回值是可以区分开的,所以返回了正确的结果。

but,以下情况将会让你大吃一惊

var arr = [[1,2, [3, 4]], new Array(1,2, 3, 4), [2,3]];
// hash 结果是 ?

你以为是 [ [ 1, 2, [ 3, 4 ] ], [1, 2, 3, 4],[ 2, 3 ] ],但其实是 [ [ 1, 2, [ 3, 4 ] ], [ 2, 3 ] ]!

惊不惊喜,意不意外!

原因也是 toString 方法造成的,[ 1, 2, [ 3, 4 ] ].toString() === [1, 2, 3, 4].toString() === '1,2,3,4'。

所以,hash方法处理意外,结果总是意外,如果结果是正确的,也有可能是意外的结果。

Set 方法面对复杂情况时,就是个呆头鹅,只管给结果里面塞。

总结一下上面的问题:

  • 1 跟 new Number(1) (同理可得 new String()等)
  • 对象数组
  • 二维数组

其实我们可以改善一下 hash方法,让他适应更多情况,其实也很简单,引起问题的根源是 hash[item] 的判断,只需要改好它,就可以得到理想的结果。

function unique(arr) {
    var hash = {}, result = [], item, type;
    for (var i = 0, l = arr.length; i < l; i++) {
        item = arr[i];
        type = typeof item.valueOf();
      // JSON.stringify 是这个方法的破绽,如果因它引起报错,那将很不适用了
        if (!hash[JSON.stringify(item) + type]) {
            hash[JSON.stringify(item) + type] = true;
          // valueOf 也可能会成为破绽之一
            typeof item !== 'object' ? result.push(item) : result.push(item.valueOf());
        }
    }
    return result;
}

// [ 1, '1', 2, 3 ]
unique([1, new Number(1), '1', new String('1'), 1, 2, 3, 2]);
// [ { a: 1 }, { b: 21 } ]
unique([{a: 1}, new Object({a: 1}), {b: 21}]);
// [ [ 1, 2, [ 3, 4 ] ], [ 1, 2, 3, 4 ], [ 2, 3 ] ]
unique([[1,2, [3,4]], new Array(1,2, 3,4), new Array(1,2, [3, 4]), [2,3]]);

至此,我们完成了一个还可以的轮子,叉会腰,表扬一下我们自己。

致敬学习!