数组去重:lodash里的uniq实现

242 阅读2分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 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实现数组去重的核心思路如下:

  1. 通过while进行数组遍历。
  2. 通过isCommon字段区分数据的存储模式。对于小于指定长度的数组(内部控制长度为200)通过普通模式实现,否则调用缓存模式进行优化。
    • 普通模式,跟我们上面手动实现的方式类似。
    • 缓存模式,如果当前环境存在Set方法,则直接调用Set进行去重,否则调用内部封装的SetCache方法,实现缓存的效果。
  3. 借助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库,直接引入该方法即可。