「前端面试题系列8」数组去重(10 种浓缩版)

图片描述

前言

这是前端面试题系列的第 8 篇,你可能错过了前面的篇章,能够在这里找到:前端

前端面试中常常会问到数组去重的问题。由于在平时的工做中遇到复杂交互的时候,须要知道该如何解决。另外,我在问应聘者这道题的时候,更多的是想考察 2 个点:对 Array 方法的熟悉程度,还有逻辑算法能力。通常我会先让应聘者说出几种方法,而后随机抽取他说的一种,具体地写一下。面试

这里有一个通用的面试技巧:本身不熟悉的东西,千万别说!我就碰到过几个应聘者,想尽量地表现本身,就说了很多方法,随机抽了一个,结果就没写出来,很尴尬。算法

ok,让咱们立刻开始今天的主题。会介绍 10 种不一样类型的方法,一些相似的方法我作了合并,写法从简到繁,其中还会有 loadsh 源码中的方法。segmentfault

10 种去重方法

假设有一个这样的数组: let originalArray = [1, '1', '1', 2, true, 'true', false, false, null, null, {}, {}, 'abc', 'abc', undefined, undefined, NaN, NaN];。后面的方法中的源数组,都是指的这个。数组

一、ES6 的 Set 对象

ES6 提供了新的数据结构 Set。它相似于数组,可是成员的值都是惟一的,没有重复的值。Set 自己是一个构造函数,用来生成 Set 数据结构。数据结构

let resultArr = Array.from(new Set(originalArray));

// 或者用扩展运算符
let resultArr = [...new Set(originalArray)];

console.log(resultArr);
// [1, "1", 2, true, "true", false, null, {…}, {…}, "abc", undefined, NaN]

Set 并非真正的数组,这里的 Array.from... 均可以将 Set 数据结构,转换成最终的结果数组。框架

这是最简单快捷的去重方法,可是细心的同窗会发现,这里的 {} 没有去重。但是又转念一想,2 个空对象的地址并不相同,因此这里并无问题,结果 ok。函数

二、Map 的 has 方法

把源数组的每个元素做为 key 存到 Map 中。因为 Map 中不会出现相同的 key 值,因此最终获得的就是去重后的结果。布局

const resultArr = new Array();

for (let i = 0; i < originalArray.length; i++) {
    // 没有该 key 值
    if (!map.has(originalArray[i])) {
        map.set(originalArray[i], true);
        resultArr.push(originalArray[i]);
    }
}

console.log(resultArr);
// [1, "1", 2, true, "true", false, null, {…}, {…}, "abc", undefined, NaN]

可是它与 Set 的数据结构比较类似,结果 ok。this

三、indexOf 和 includes

创建一个新的空数组,遍历源数组,往这个空数组里塞值,每次 push 以前,先判断是否已有相同的值。

判断的方法有 2 个:indexOf 和 includes,但它们的结果之间有细微的差异。先看 indexOf。

const resultArr = [];
for (let i = 0; i < originalArray.length; i++) {
    if (resultArr.indexOf(originalArray[i]) < 0) {
        resultArr.push(originalArray[i]);
    }
}
console.log(resultArr);
// [1, "1", 2, true, "true", false, null, {…}, {…}, "abc", undefined, NaN, NaN]

indexOf 并不没处理 NaN

再来看 includes,它是在 ES7 中正式提出的。

const resultArr = [];
for (let i = 0; i < originalArray.length; i++) {
    if (!resultArr.includes(originalArray[i])) {
        resultArr.push(originalArray[i]);
    }
}
console.log(resultArr);
// [1, "1", 2, true, "true", false, null, {…}, {…}, "abc", undefined, NaN]

includes 处理了 NaN,结果 ok。

四、sort

先将原数组排序,生成新的数组,而后遍历排序后的数组,相邻的两两进行比较,若是不一样则存入新数组。

const sortedArr = originalArray.sort();

const resultArr = [sortedArr[0]];

for (let i = 1; i < sortedArr.length; i++) {
    if (sortedArr[i] !== resultArr[resultArr.length - 1]) {
        resultArr.push(sortedArr[i]);
    }
}
console.log(resultArr);
// [1, "1", 2, NaN, NaN, {…}, {…}, "abc", false, null, true, "true", undefined]

从结果能够看出,对源数组进行了排序。但一样的没有处理 NaN

五、双层 for 循环 + splice

双层循环,外层遍历源数组,内层从 i+1 开始遍历比较,相同时删除这个值。

for (let i = 0; i < originalArray.length; i++) {
    for (let j = (i + 1); j < originalArray.length; j++) {
        // 第一个等于第二个,splice去掉第二个
        if (originalArray[i] === originalArray[j]) {
            originalArray.splice(j, 1);
            j--;
        }
    }
}

console.log(originalArray);
// [1, "1", 2, true, "true", false, null, {…}, {…}, "abc", undefined, NaN, NaN]

splice 方法会修改源数组,因此这里咱们并无新开空数组去存储,最终输出的是修改以后的源数组。但一样的没有处理 NaN

六、原始去重

定义一个新数组,并存放原数组的第一个元素,而后将源数组一一和新数组的元素对比,若不一样则存放在新数组中。

let resultArr = [originalArray[0]];
for(var i = 1; i < originalArray.length; i++){
    var repeat = false;
    for(var j=0; j < resultArr.length; j++){
        if(originalArray[i] === resultArr[j]){
            repeat = true;
            break;
        }
    }

    if(!repeat){
       resultArr.push(originalArray[i]);
    }
}
console.log(resultArr);
// [1, "1", 2, true, "true", false, null, {…}, {…}, "abc", undefined, NaN, NaN]

这是最原始的去重方法,很好理解,但写法繁琐。一样的没有处理 NaN

七、ES5 的 reduce

reduce 是 ES5 中方法,经常使用于值的累加。它的语法:

arr.reduce(callback[, initialValue])

reduce 的第一个参数是一个 callback,callback 中的参数分别为: Accumulator(累加器)、currentValue(当前正在处理的元素)、currentIndex(当前正在处理的元素索引,可选)、array(调用 reduce 的数组,可选)。

reduce 的第二个参数,是做为第一次调用 callback 函数时的第一个参数的值。若是没有提供初始值,则将使用数组中的第一个元素。

利用 reduce 的特性,再结合以前的 includes(也能够用 indexOf),就能获得新的去重方法:

const reducer = (acc, cur) => acc.includes(cur) ? acc : [...acc, cur];

const resultArr = originalArray.reduce(reducer, []);

console.log(resultArr);
// [1, "1", 2, true, "true", false, null, {…}, {…}, "abc", undefined, NaN]

这里的 [] 就是初始值(initialValue)。acc 是累加器,在这里的做用是将没有重复的值塞入新数组(它一开始是空的)。 reduce 的写法很简单,但须要多加理解。它能够处理 NaN,结果 ok。

八、对象的属性

每次取出原数组的元素,而后在对象中访问这个属性,若是存在就说明重复。

const resultArr = [];
const obj = {};
for(let i = 0; i < originalArray.length; i++){
    if(!obj[originalArray[i]]){
        resultArr.push(originalArray[i]);
        obj[originalArray[i]] = 1;
    }
}
console.log(resultArr);
// [1, 2, true, false, null, {…}, "abc", undefined, NaN]

但这种方法有缺陷。从结果看,它貌似只关心值,不关注类型。还把 {} 给处理了,但这不是正统的处理办法,因此 不推荐使用

九、filter + hasOwnProperty

filter 方法会返回一个新的数组,新数组中的元素,经过 hasOwnProperty 来检查是否为符合条件的元素。

const obj = {};
const resultArr = originalArray.filter(function (item) {
    return obj.hasOwnProperty(typeof item + item) ? false : (obj[typeof item + item] = true);
});

console.log(resultArr);
// [1, "1", 2, true, "true", false, null, {…}, "abc", undefined, NaN]

貌似 是目前看来最完美的解决方案了。这里稍加解释一下:

  • hasOwnProperty 方法会返回一个布尔值,指示对象自身属性中是否具备指定的属性。
  • typeof item + item 的写法,是为了保证值相同,但类型不一样的元素被保留下来。例如:第一个元素为 number1,第二第三个元素都是 string1,因此第三个元素就被去除了。
  • obj[typeof item + item] = true 若是 hasOwnProperty 没有找到该属性,则往 obj 里塞键值对进去,以此做为下次循环的判断依据。
  • 若是 hasOwnProperty 没有检测到重复的属性,则告诉 filter 方法能够先积攒着,最后一块儿输出。

看似 完美解决了咱们源数组的去重问题,但在实际的开发中,通常不会给两个空对象给咱们去重。因此稍加改变源数组,给两个空对象中加入键值对。

let originalArray = [1, '1', '1', 2, true, 'true', false, false, null, null, {a: 1}, {a: 2}, 'abc', 'abc', undefined, undefined, NaN, NaN];

而后再用 filter + hasOwnProperty 去重。

然而,结果居然把 {a: 2} 给去除了!!!这就不对了。

因此,这种方法有点去重 过头 了,也是存在问题的。

十、lodash 中的 _.uniq

灵机一动,让我想到了 lodash 的去重方法 _.uniq,那就尝试一把:

console.log(_.uniq(originalArray));

// [1, "1", 2, true, "true", false, null, {…}, {…}, "abc", undefined, NaN]

用法很简单,能够在实际工做中正确处理去重问题。

而后,我在好奇心促使下,看了它的源码,指向了 baseUniq 文件,它的源码以下:

function baseUniq(array, iteratee, comparator) {
  let index = -1
  let includes = arrayIncludes
  let isCommon = true

  const { length } = array
  const result = []
  let seen = result

  if (comparator) {
    isCommon = false
    includes = arrayIncludesWith
  }
  else if (length >= LARGE_ARRAY_SIZE) {
    const 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) {
    let value = array[index]
    const computed = iteratee ? iteratee(value) : value

    value = (comparator || value !== 0) ? value : 0
    if (isCommon && computed === computed) {
      let 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.push(computed)
      }
      result.push(value)
    }
  }
  return result
}

有比较多的干扰项,那是为了兼容另外两个方法,_.uniqBy 和 _.uniqWith。去除掉以后,就会更容易发现它是用 while 作了循环。当遇到相同的值得时候,continue outer 再次进入循环进行比较,将没有重复的值塞进 result 里,最终输出。

另外,_.uniqBy 方法能够经过指定 key,来专门去重对象列表。

_.uniqBy([{ 'x': 1 }, { 'x': 2 }, { 'x': 1 }], 'x');
// => [{ 'x': 1 }, { 'x': 2 }]

_.uniqWith 方法能够彻底地给对象中全部的键值对,进行比较。

var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }, { 'x': 1, 'y': 2 }];

_.uniqWith(objects, _.isEqual);
// => [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }]

这两个方法,都还挺实用的。

总结

从上述的这些方法来看,ES6 开始出现的方法(如 Set、Map、includes),都能完美地解决咱们平常开发中的去重需求,关键它们还都是原生的,写法还更简单。

因此,咱们提倡拥抱原生,由于它们真的没有那么难以理解,至少在这里我以为它比 lodash 里 _.uniq 的源码要好理解得多,关键是还能解决问题。

PS:欢迎关注个人公众号 “超哥前端小栈”,交流更多的想法与技术。

相关文章
相关标签/搜索