原生 js 实现对象数组去重

最近研究了一下如何实现对象数组的去重,这里分享一下经验,本文没有使用诸如 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

你可能感兴趣的:(原生 js 实现对象数组去重)