持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第1天,点击查看活动详情
前言
数组去重在实际的业务场景中经常出现,对于哪种实现可以凭个人喜好,不过为了直观和代码的简练,原生实现上比较推荐以Set
的实现方式,并对其进行函数封装和抽离,可实现简练直观的调用。
大多数项目中可能存在lodash工具库,从严谨性兼容性上看,可以直接引入uniq
方法。
lodash里的uniq
方法可以实现数组去重,并且返回的结果是新的数组。
_.uniq([2, 1, 2]);
// => [2, 1]
手动实现
实现一个数组去重的方法,核心在于遍历与标记,实现如下:
function uniq(arr) {
if (!Array.isArray(arr)) return
const result = []
const map = {}
for (let i = 0; i < arr.length; i++) {
const value = arr[i]
if (!map[value]) {
result.push(value)
map[value] = 1
} else {
map[value] += 1
}
}
return result
}
由于存储数据的映射关系我们用了普通对象,对于对象等其他值,在充当对象的key值时会调用其自身的toString方法,导致当后续数组项是对象时会认为数组项已出现过而忽略。
此时我们可以用Map进行唯一存储:
function uniq(arr) {
if (!Array.isArray(arr)) return
const result = []
const map = new Map()
for (let i = 0; i < arr.length; i++) {
const value = arr[i]
if (!map.has(value)) {
result.push(value)
map.set(value, 1)
} else {
const count = map.get(value) + 1
map.set(value, count)
}
}
return result
}
lodash里的uniq
lodash实现数组去重的核心思路如下:
- 通过
while
进行数组遍历。 - 通过
isCommon
字段区分数据的存储模式。对于小于指定长度的数组(内部控制长度为200)通过普通模式实现,否则调用缓存模式进行优化。- 普通模式,跟我们上面手动实现的方式类似。
- 缓存模式,如果当前环境存在Set方法,则直接调用Set进行去重,否则调用内部封装的
SetCache
方法,实现缓存的效果。
- 借助
arrayIncludes
内部方法是对Array.includes的模拟封装,便于对数组某一项存在性的判断。
缓存判断的方法:cacheHas
cacheHas方法主要是来判断缓对象里是否存在指定项。
源码如下:
function cacheHas(cache, key) {
return cache.has(key)
}
set实例转数组的方法:setToArray
setToArray方法的实现主要是通过调用传入参数Set
实例上的forEach
方法遍历每一项,然后依次添加到数组result里,遍历完毕返回其结果。
实际上如果不考虑兼容性,我们可以用展开语法进行处理,代码如下:
function setToArray(set){
return [...set]
}
lodash里setToArray的实现:
function setToArray(set) {
let index = -1
const result = new Array(set.size)
set.forEach((value) => {
result[++index] = value
})
return result
}
set实例的创建:createSet
createSet方法是一个工厂函数
,实现上会根据当前环境判断是否存在Set
构造函数,不存在的话创建的Set实例本质就是一个对象。
源码如下:
import setToArray from './setToArray.js'
const INFINITY = 1 / 0
const createSet = (Set && (1 / setToArray(new Set([,-0]))[1]) == INFINITY)
? (values) => new Set(values)
: () => {}
uniq源码实现
import SetCache from './_SetCache.js';
import arrayIncludes from './_arrayIncludes.js';
import arrayIncludesWith from './_arrayIncludesWith.js';
import cacheHas from './_cacheHas.js';
import createSet from './_createSet.js';
import setToArray from './_setToArray.js';
var LARGE_ARRAY_SIZE = 200;
// 第二个参数iteratee是迭代器,在迭代时调用
// 第三个参数comparator是比较器,可以在迭代时比较每个元素
function baseUniq(array, iteratee, comparator) {
var index = -1,
includes = arrayIncludes,
length = array.length,
isCommon = true, // 是否是普通模式
result = [], // 返回结果存储的数组
seen = result; // 初始化为返回结果一致的数组,在普通模式下其就是和result一样的空数组,在缓存模式下其是缓存数组,通过new SetCache得到
if (comparator) {
// 存在比较器comparator时的处理
isCommon = false;
includes = arrayIncludesWith;
}
else if (length >= LARGE_ARRAY_SIZE) {
// 缓存模式,当前环境存在set的话直接调用并返回
var set = iteratee ? null : createSet(array);
if (set) {
return setToArray(set);
}
isCommon = false;
includes = cacheHas;
seen = new SetCache;
}
else {
seen = iteratee ? [] : result;
}
outer:
while (++index < length) {
// 遍历数组,存在iteratee迭代器时会调用并赋值
var value = array[index],
computed = iteratee ? iteratee(value) : value;
value = (comparator || value !== 0) ? value : 0;
if (isCommon && computed === computed) {
// 该判断下的seen本质和result一样同为普通数组
var seenIndex = seen.length;
while (seenIndex--) {
// 本质就是在结果里进一步查找是否存在指定项
if (seen[seenIndex] === computed) {
continue outer;
}
}
if (iteratee) {
seen.push(computed);
}
result.push(value);
}
else if (!includes(seen, computed, comparator)) {
if (seen !== result) {
此时的seen是缓存数组
seen.push(computed);
}
result.push(value);
}
}
return result;
}
数组去重的其他方法实现
Array.prototype.indexOf
通过indexOf检索存在项,实现如下:
function uniq(arr) {
if (!Array.isArray(arr)) return
const result = []
for (let i = 0; i < arr.length; i++) {
const value = arr[i]
if (result.indexOf(value) < 0) {
result.push(value)
}
}
return result
}
Array.prototype.includes
同样的,通过includes检索存在项,实现如下:
function uniq(arr) {
if (!Array.isArray(arr)) return
const result = []
for (let i = 0; i < arr.length; i++) {
const value = arr[i]
if (!result.includes(value)) {
result.push(value)
}
}
return result
}
Array.prototype.filter
通过过滤排除已存在项,实现如下:
function uniq(arr) {
if (!Array.isArray(arr)) return
return arr.filter((item, index) => arr.indexOf(item) === index)
}
Array.prototype.reduce
通过reduce遍历,通过第二个参数传递数组实现遍历时的数组操作。
function uniq(arr) {
if (!Array.isArray(arr)) return
return arr.reduce((prev, current) => {
if (!prev.includes(current)) {
prev.push(current)
}
return prev
}, [])
}
Set
通过Set实现去重,再通过Array.from或者通过展开语法实现类数组向数组的转换。
function uniq(arr) {
if (!Array.isArray(arr)) return
return [...new Set(arr)] // Array.from(new Set(arr))
}
小结
本篇章我们了解到数组去重的思路,同时了解lodash里对数组去重方法的实现,对于实际开发中我们可以借助方法调用的形式实现数组去重,在代码阅读上更加直观。
我们可以根据实际情况在当前项目中添加uniq方法,如果当前项目存在lodash库,直接引入该方法即可。