我有一个非常简单的 JavaScript 数组,它可能包含也可能不包含重复项。
var names = ["Mike","Matt","Nancy","Adam","Jenny","Nancy","Carl"];
我需要删除重复项并将唯一值放入一个新数组中。
我可以指出我尝试过的所有代码,但我认为它没有用,因为它们不起作用。我也接受 jQuery 解决方案。
使用Set构造函数和展开语法:
uniq = [...new Set(array)];
uniqueArray = a.filter(function(item, pos) { return a.indexOf(item) == pos; })
基本上,我们遍历数组,并且对于每个元素,检查该元素在数组中的第一个位置是否等于当前位置。显然,这两个位置对于重复元素是不同的。
使用过滤器回调的第三个(“这个数组”)参数,我们可以避免数组变量的关闭:
uniqueArray = a.filter(function(item, pos, self) { return self.indexOf(item) == pos; })
虽然简洁,但该算法对于大型数组(二次时间)并不是特别有效。
function uniq(a) { var seen = {}; return a.filter(function(item) { return seen.hasOwnProperty(item) ? false : (seen[item] = true); }); }
这就是通常的做法。这个想法是将每个元素放在一个哈希表中,然后立即检查它的存在。这给了我们线性时间,但至少有两个缺点:
uniq([1,"1"])
[1]
uniq([{foo:1},{foo:2}])
[{foo:1}]
也就是说,如果您的数组只包含原语并且您不关心类型(例如它始终是数字),那么这个解决方案是最佳的。
一个通用的解决方案结合了这两种方法:它使用散列查找来查找原语和线性搜索对象。
function uniq(a) { var prims = {"boolean":{}, "number":{}, "string":{}}, objs = []; return a.filter(function(item) { var type = typeof item; if(type in prims) return prims[type].hasOwnProperty(item) ? false : (prims[type][item] = true); else return objs.indexOf(item) >= 0 ? false : objs.push(item); }); }
另一种选择是先对数组进行排序,然后删除与前一个元素相等的每个元素:
function uniq(a) { return a.sort().filter(function(item, pos, ary) { return !pos || item != ary[pos - 1]; }); }
同样,这不适用于对象(因为所有对象都等于sort)。此外,我们默默地更改原始数组作为副作用 - 不好!但是,如果您的输入已经排序,这是要走的路(只需sort从上面删除)。
sort
有时需要基于某些标准而不是仅相等性来唯一化列表,例如,过滤掉不同但共享某些属性的对象。这可以通过传递回调优雅地完成。此“键”回调应用于每个元素,并删除具有相同“键”的元素。由于key预计会返回一个原语,因此哈希表在这里可以正常工作:
key
function uniqBy(a, key) { var seen = {}; return a.filter(function(item) { var k = key(item); return seen.hasOwnProperty(k) ? false : (seen[k] = true); }) }
一个特别有用key()的方法是JSON.stringify删除物理上不同但“看起来”相同的对象:
key()
JSON.stringify
a = [[1,2,3], [4,5,6], [1,2,3]] b = uniqBy(a, JSON.stringify) console.log(b) // [[1,2,3], [4,5,6]]
如果key不是原始的,则必须求助于线性搜索:
function uniqBy(a, key) { var index = []; return a.filter(function (item) { var k = key(item); return index.indexOf(k) >= 0 ? false : index.push(k); }); }
在 ES6 中,您可以使用Set:
Set
function uniqBy(a, key) { let seen = new Set(); return a.filter(item => { let k = key(item); return seen.has(k) ? false : seen.add(k); }); }
或Map:
Map
function uniqBy(a, key) { return [ ...new Map( a.map(x => [key(x), x]) ).values() ] }
这两者也适用于非原始键。
通过键删除对象时,您可能希望保留“相等”对象中的第一个或最后一个。
使用Set上面的变体保留第一个,并Map保留最后一个:
function uniqByKeepFirst(a, key) { let seen = new Set(); return a.filter(item => { let k = key(item); return seen.has(k) ? false : seen.add(k); }); } function uniqByKeepLast(a, key) { return [ ...new Map( a.map(x => [key(x), x]) ).values() ] } // data = [ {a:1, u:1}, {a:2, u:2}, {a:3, u:3}, {a:4, u:1}, {a:5, u:2}, {a:6, u:3}, ]; console.log(uniqByKeepFirst(data, it => it.u)) console.log(uniqByKeepLast(data, it => it.u))
underscore和Lo-Dash都提供了uniq方法。他们的算法基本上类似于上面的第一个片段,归结为:
uniq
var result = []; a.forEach(function(item) { if(result.indexOf(item) < 0) { result.push(item); } });
这是二次方的,但还有一些不错的附加功能,例如包装 native indexOf、通过键唯一化的能力(iteratee用他们的说法)以及对已排序数组的优化。
indexOf
iteratee
如果你在使用 jQuery 并且在它前面没有一美元就无法忍受任何东西,它是这样的:
$.uniqArray = function(a) { return $.grep(a, function(item, pos) { return $.inArray(item, a) === pos; }); }
这也是第一个片段的变体。
JavaScript 中的函数调用很昂贵,因此上述解决方案虽然简洁,但并不是特别有效。为了获得最佳性能,请filter用循环替换并摆脱其他函数调用:
filter
function uniq_fast(a) { var seen = {}; var out = []; var len = a.length; var j = 0; for(var i = 0; i < len; i++) { var item = a[i]; if(seen[item] !== 1) { seen[item] = 1; out[j++] = item; } } return out; }
这段丑陋的代码与上面的代码片段 #3 相同,但速度快了一个数量级(截至 2017 年,它的速度只有两倍 - JS 核心人员做得很好!)
function uniq(a) { var seen = {}; return a.filter(function(item) { return seen.hasOwnProperty(item) ? false : (seen[item] = true); }); } function uniq_fast(a) { var seen = {}; var out = []; var len = a.length; var j = 0; for(var i = 0; i < len; i++) { var item = a[i]; if(seen[item] !== 1) { seen[item] = 1; out[j++] = item; } } return out; } ///// var r = [0,1,2,3,4,5,6,7,8,9], a = [], LEN = 1000, LOOPS = 1000; while(LEN--) a = a.concat(r); var d = new Date(); for(var i = 0; i < LOOPS; i++) uniq(a); document.write('<br>uniq, ms/loop: ' + (new Date() - d)/LOOPS) var d = new Date(); for(var i = 0; i < LOOPS; i++) uniq_fast(a); document.write('<br>uniq_fast, ms/loop: ' + (new Date() - d)/LOOPS)
ES6 提供了Set对象,这让事情变得简单多了:
function uniq(a) { return Array.from(new Set(a)); }
或者
let uniq = a => [...new Set(a)];
请注意,与 python 不同,ES6 集合是按插入顺序迭代的,因此此代码保留了原始数组的顺序。
但是,如果您需要一个具有唯一元素的数组,为什么不从一开始就使用集合呢?
uniq可以在相同的基础上构建基于生成器的“惰性”版本:
function* uniqIter(a) { let seen = new Set(); for (let x of a) { if (!seen.has(x)) { seen.add(x); yield x; } } } // example: function* randomsBelow(limit) { while (1) yield Math.floor(Math.random() * limit); } // note that randomsBelow is endless count = 20; limit = 30; for (let r of uniqIter(randomsBelow(limit))) { console.log(r); if (--count === 0) break } // exercise for the reader: what happens if we set `limit` less than `count` and why