最近研究了一下如何实现对象数组的去重,这里分享一下经验,本文没有使用诸如 lodash
之类的第三方库,仅使用 ES6 中的语法。
假如有如下数组:
const originArray = [
{ id: 1, value: 'a' },
{ id: 1, value: 'b' },
{ id: 2, value: 'c' },
{ id: 3, value: 'd' },
{ id: 3, value: 'e' },
{ id: 4, value: 'f' }
]
我们要根据其中的元素对象的 id
键进行数组去重,这里我们保留索引靠前的重复元素。
最终实现
这里直接给出最终实现,有需要的可以使用:
/**
* 根据 id 进行数组去重
* @param {array} origin 原始数组
*/
const unique = function (origin) {
let temp = {}
return origin.reverse().filter(item => (item.id in temp) ? false : (temp[item.id] = true))
}
实现原理
接下来我们来分析下上面的实现并解释下原理。这种实现方法本质上利用了 对象的键唯一性。通过在临时数组中添加数组的元素 id 作为键来实现数组去重。
一旦发现 id 已经存在了,就说明该元素是重复元素,直接剔除。如果发现临时数组中没有 id,说明没有重复,就把自己保留下来。
实现细节
这个实现里有两个细节需要注意一下:
首先是我们使用了reverse()
对原始数组进行了倒序操作。这个原因是因为 js 中的集合迭代方法是从后往前进行的。也就是说,诸如 .map
、.filter
等方法都是从数组的最后一个元素开始进行遍历的。而我们的目的是保留重复元素中索引靠前的哪一个,所以需要先进行倒序。如果你没有这个需求或者需要保留索引靠后的重复元素,那么就不需要添加reverse()
了。
第二个细节是后面的三元表达式:
(item.id in temp) ? false : (temp[item.id] = true)
这里能将判断压缩进一行的原因在于后面的temp[item.id] = true
。在 js 里,赋值操作是有返回值的,并且其返回值即为赋值操作的右值(这也是为什么 js 里可以连等赋值)。也就是说,a = true
将会返回 true。在这里我们就是使用了temp[item.id] = true
的返回值为 true 来实现了保留目标元素的功能。
性能对比
接下来比较几种常见的原生对象数组去重的方法,先请出其余两位对比组实现:
/**
* 根据 id 进行数组去重 对比1
* @param {array} origin 原始数组
*/
function uniqueA(origin) {
// 临时数组
let tempObj = { }
// 倒序后遍历数组进行赋值
// 以 id 的值为键,以数组元素的索引为值
// 老的重复元素会因为键重复而被覆盖
origin.reverse().map((item, index) => tempObj[item.id] = index)
// 根据去重后的索引值提取出目标数组
return Object.values(tempObj).map(index => origin[index])
}
/**
* 根据 id 进行数组去重 对比2
* @param {array} origin 原始数组
*/
function uniqueB(origin) {
// 先提取出 id 数组
return origin.map(item => item.id)
// 如果该 id 的索引是原数组中第一个该 id 的索引,就返回其索引,否则返回 null
.map((id, index, arr) => (arr.indexOf(id, 0) === index) ? index : null)
// 移除上一步产生的 null
.filter(Boolean)
// 根据去重后的索引提取出目标数组
.map(index => origin[index])
}
接下来增添一些测试所需的辅助函数,下面的代码你可以自行复制进行测试。这里我所用的随机测试数组长度为 10,000,每个方法执行 100 次,统计其总用时。
// 执行三种方法并返回其用时
showUniqueUsed(unique)
showUniqueUsed(uniqueA)
showUniqueUsed(uniqueB)
/**
* 显示去重所需时间
* @param {function} func 进行去重的函数
* @param {number} time 循环执行多少次
* @param {number} length 要去重的随机数组长度
*/
function showUniqueUsed(func, time = 100, length = 10000) {
console.time('uniqueUsed')
for (let i = 0; i < time; i ++) {
func(getRandomArray(length))
}
console.timeEnd('uniqueUsed')
}
/**
* 获取指定长度的数组,元素形式如下
* {
* id: 12332, // 0 ~ 指定长度之间的随机整数
* value: 12332 // 该元素的索引值
* }
* @param {number} length 数组长度
*/
function getRandomArray(length) {
return Array(length).fill(null).map((item, index) => ({
id: Math.floor(Math.random() * (length + 1)),
value: index
}))
}
/**
* 根据 id 进行数组去重 本文实现
* @param {array} origin 原始数组
*/
function unique(origin) {
let temp = {}
return origin.filter(item => (item.id in temp) ? false : (temp[item.id] = true))
}
/**
* 根据 id 进行数组去重 对比1
* @param {array} origin 原始数组
*/
function uniqueA(origin) {
let tempObj = {}
origin.reverse().map((item, index) => tempObj[item.id] = index)
return Object.values(tempObj).map(index => origin[index])
}
/**
* 根据 id 进行数组去重 对比2
* @param {array} origin 原始数组
*/
function uniqueB(origin) {
return origin.map(item => item.id)
.map((id, index, arr) => (arr.indexOf(id, 0) === index) ? index : null)
.filter(Boolean)
.map(index => origin[index])
}
以下是测试结果,可以看到本文中给出的方法用时最短:
uniqueUsed: 153.780ms
uniqueUsed: 219.848ms
uniqueUsed: 4665.961ms