原生 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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342