开始吧
总所周知,借助 Set 对象不可包含重复元素的特性,可以以简单的逻辑实现基本的数组去重
const uniq = <T>(list: T[]) => {
return Array.from(new Set(list))
}
const list = [1, 2, 2, 4, 5, 5, 6]
uniq(list) // => [1, 2, 4, 5, 6]
但在实际的应用中,情况往往会复杂一些。我们可能不会将数组中的元素本身直接进行比较去重,而是以元素转换后的某个值为作为基准判断是否去重,例如:
-
去除 number 数组中绝对值相同的元素
-
去除 object 数组中属性
id
相同的元素
以场景 1 为例,我们可以定义两个列表,一个用于存放 number 元素的绝对值,另一个则存放原始值。遍历目标数组,为每一个元素计算其绝对值,并与现存绝对值数组中的元素比对,判断是否已存在。若不存在,则将计算后的绝对值和原始值分别添加到对应的列表中,否则跳过当前元素,最后将原始值列表返回
const uniq = (list: number[]) => {
const set = new Set<number>()
const result = []
for (const item of list) {
const abs = Math.abs(item)
if (!set.has(abs)) {
set.add(abs)
result.push(item)
}
}
return result
}
const list = [-5, -2, 1, 2, 5, 6]
uniq(list) // => [ -5, -2, 1, 6 ]
回到上述场景 2,这里的逻辑与前一个场景相似,不同之处在于数组的元素类型由 number 变成了 object,计算 number 绝对值的操作被替换为取得 object 的属性 id
,很明显将这部分元素转换为比较值的操作从函数中抽离,便可以得到一个通用的数组去重函数 uniqBy
const uniqBy = <T>(fn: (item: T) => any, list: T[]) => {
const set = new Set()
const result: T[] = []
for (const item of list) {
const appliedItem = fn(item)
if (!set.has(appliedItem)) {
set.add(appliedItem)
result.push(item)
}
}
return result
}
这样类似的问题都可以得到一个通解
const list1 = [-5, -2, 1, 2, 5, 6]
uniqBy(Math.abs, list1) // => [ -5, -2, 1, 6 ]
const list2 = [
{ id: 1, name: 'a' },
{ id: 1, name: 'b' },
{ id: 2, name: 'c' },
{ id: 2, name: 'd' }
]
uniqBy(({ id }) => id, list2) // => [ { id: 1, name: 'a' }, { id: 2, name: 'c' } ]
思考一下,我们继续拓展上述场景 2 中的问题,对于一个对象数组,如果我们不只是想要在对象的 id
相同时就将元素移除,而是在 id
和 name
都相同时才进行去重操作,即预期结果如下
const list = [
{ id: 1, name: 'a' },
{ id: 1, name: 'b' },
{ id: 2, name: 'c' },
{ id: 2, name: 'c' }
]
// => [ { id: 1, name: 'a' }, { id: 1, name: 'b' }, { id: 2, name: 'c' } ]
那么我们上文定义的去重函数也许无法继续在当前场景中生效。因为这里需要涉及数组元素间的直接对比,我们需要一个更加灵活,自由度更高的通用函数
与上文逻辑相似,我们定义一个函数 uniqWith
,函数中仍然遍历传入的目标数组,并对每个元素判断是否已存在于结果数组中,若不存在,则将其添加到数组中,最后返回结果数组。不同之处在于,这次我们拓展了判断元素是否存在的逻辑,接收一个自定义的判重函数,对结果数组中的元素与当前遍历元素依次调用对比,以判断是否需要去重
const includesWith = <T>(fn: (a: T, b: T) => boolean, x: T, list: T[]) => {
for (const item of list) {
if (fn(x, item)) {
return true
}
}
return false
}
const uniqWith = <T>(fn: (a: T, b: T) => boolean, list: T[]) => {
const result: T[] = []
for (const item of list) {
if (!includesWith(fn, item, result)) {
result.push(item)
}
}
return result
}
这样,这个函数便能够解决上文场景中的需求
const list = [
{ id: 1, name: 'a' },
{ id: 1, name: 'b' },
{ id: 2, name: 'c' },
{ id: 2, name: 'c' }
]
uniqWith((a, b) => a.id === b.id && a.name === b.name, list)
// => [ { id: 1, name: 'a' }, { id: 1, name: 'b' }, { id: 2, name: 'c' } ]
同理 ,因为 uniqWith
比 uniqBy
逻辑上更偏底层,所以 uniqBy
也可以直接套用 uniqWith
来实现
const uniqBy = <T>(fn: (item: T) => any, list: T[]) => uniqWith((a, b) => fn(a) === fn(b), list)
const list1 = [-5, -2, 1, 2, 5, 6]
uniqBy(Math.abs, list1) // => [ -5, -2, 1, 6 ]
至此,文中定义的几种去重函数便可以覆盖大部分的调用需求,调用者可以根据场景选择更简洁的顶层调用或者更灵活的底层调用,结合函数柯里化也可以灵活地生成自定义顶层逻辑的偏函数
结语
本文中使用去重(uniq)函数作为样例,但更多的是体现一种函数的设计思想,而这种思想也可以应用到其它的函数实现中。
最后,感谢您的阅读!