长话短说
使用 Set 构造函数和 扩展语法 :
uniq = [...new Set(array)];
(请注意,var uniq 将是一个数组... new Set() 将其变成一个集合,但 [...] 将其再次变成一个数组)
“聪明”但天真的方式
uniqueArray = a.filter(function(item, pos) {
return a.indexOf(item) == pos;
})
基本上,我们遍历数组,并针对每个元素检查该元素在数组中的第一个位置是否等于当前位置。显然,对于重复元素,这两个位置是不同的。
使用过滤器回调的第三个参数(“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);
});
}
通常的做法是这样的。其思路是将每个元素放入哈希表中,然后立即检查其是否存在。这为我们提供了线性时间,但至少有两个缺点:
-
由于 JavaScript 中的哈希键只能是字符串或符号,因此此代码不区分数字和“数字字符串”。也就是说,
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\' 回调应用于每个元素,并且删除具有相等 \'keys\' 的元素。由于 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
不是原始的,那么你必须采取线性搜索:
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))
图书馆
Underscore underscore 都 Lo-Dash 提供了 uniq
方法。它们的算法基本类似于上面的第一个代码片段,可以归结为以下内容:
var result = [];
a.forEach(function(item) {
if(result.indexOf(item) < 0) {
result.push(item);
}
});
这是二次的,但有一些很好的附加优点,比如包装本机 indexOf
、按键唯一化的能力( 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
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