我有一个非常简单的 JavaScript 数组,可能包含也可能不包含重复项。
var names = ["Mike","Matt","Nancy","Adam","Jenny","Nancy","Carl"];
我需要删除重复项并将唯一值放在一个新数组中。
我可以指出我尝试过的所有代码,但我认为它没用,因为它们不起作用。我也接受 jQuery 解决方案。
uniq = [...new Set(array)];
uniqueArray = a.filter(function(item, pos) {
return a.indexOf(item) == pos;
})
基本上,我们迭代数组,并为每个元素检查数组中此元素的第一个位置是否等于当前位置。显然,这两个位置对于重复元素是不同的。
使用过滤器回调的第 3 个(“this array”)参数,我们可以避免数组变量的闭包:
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
)。
有时需要根据除了相等之外的某些标准来统一列表,例如,过滤掉不同的对象,但共享一些属性。这可以通过传递回调来优雅地完成。此 “键” 回调应用于每个元素,并删除具有相同 “键” 的元素。由于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
,它将删除物理上不同的对象,但 “看起来” 相同:
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
不是原始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
:
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
:
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))
下划线和Lo-Dash 都提供uniq
方法。他们的算法基本上类似于上面的第一个片段,归结为:
var result = [];
a.forEach(function(item) {
if(result.indexOf(item) < 0) {
result.push(item);
}
});
这是二次方的,但还有很好的额外好处,比如包装本机indexOf
,通过密钥iteratee
能力(在他们的说法中是iteratee
),以及已经排序的数组的优化。
如果你正在使用 jQuery 并且在它之前没有一美元就无法忍受任何事情,那就是这样的:
$.uniqArray = function(a) {
return $.grep(a, function(item, pos) {
return $.inArray(item, a) === pos;
});
}
这也是第一个片段的变体。
函数调用在 JavaScript 中很昂贵,因此上述解决方案虽然简洁,但效率不高。为了获得最佳性能,请使用循环替换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
使用 jQuery 快速而肮脏:
var names = ["Mike","Matt","Nancy","Adam","Jenny","Nancy","Carl"];
var uniqueNames = [];
$.each(names, function(i, el){
if($.inArray(el, uniqueNames) === -1) uniqueNames.push(el);
});
厌倦了看到所有使用 for 循环或 jQuery 的坏例子。 Javascript 现在拥有完美的工具:排序,映射和缩小。
var names = ["Mike","Matt","Nancy","Adam","Jenny","Nancy","Carl"];
var uniq = names.reduce(function(a,b){
if (a.indexOf(b) < 0 ) a.push(b);
return a;
},[]);
console.log(uniq, names) // [ 'Mike', 'Matt', 'Nancy', 'Adam', 'Jenny', 'Carl' ]
// one liner
return names.reduce(function(a,b){if(a.indexOf(b)<0)a.push(b);return a;},[]);
可能有更快的方法,但这个很不错。
var uniq = names.slice() // slice makes copy of array before sorting it
.sort(function(a,b){
return a > b;
})
.reduce(function(a,b){
if (a.slice(-1)[0] !== b) a.push(b); // slice(-1)[0] means last item in array without removing it (like .pop())
return a;
},[]); // this empty array becomes the starting value for a
// one liner
return names.slice().sort(function(a,b){return a > b}).reduce(function(a,b){if (a.slice(-1)[0] !== b) a.push(b);return a;},[]);
在 ES6 中,你有集合和传播,这使得删除所有重复项非常容易和高效:
var uniq = [ ...new Set(names) ]; // [ 'Mike', 'Matt', 'Nancy', 'Adam', 'Jenny', 'Carl' ]
有人询问是根据有多少个唯一名称来排序结果:
var names = ['Mike', 'Matt', 'Nancy', 'Adam', 'Jenny', 'Nancy', 'Carl']
var uniq = names
.map((name) => {
return {count: 1, name: name}
})
.reduce((a, b) => {
a[b.name] = (a[b.name] || 0) + b.count
return a
}, {})
var sorted = Object.keys(uniq).sort((a, b) => uniq[a] < uniq[b])
console.log(sorted)