「前端面试题系列8」数组去重(10 种浓缩版)
栏目: JavaScript · 发布时间: 6年前
内容简介:这是前端面试题系列的第 8 篇,你可能错过了前面的篇章,可以在这里找到:前端面试中经常会问到数组去重的问题。因为在平时的工作中遇到复杂交互的时候,需要知道该如何解决。另外,我在问应聘者这道题的时候,更多的是想考察 2 个点:对 Array 方法的熟悉程度,还有逻辑算法能力。一般我会先让应聘者说出几种方法,然后随机抽取他说的一种,具体地写一下。
前言
这是前端面试题系列的第 8 篇,你可能错过了前面的篇章,可以在这里找到:
前端面试中经常会问到数组去重的问题。因为在平时的工作中遇到复杂交互的时候,需要知道该如何解决。另外,我在问应聘者这道题的时候,更多的是想考察 2 个点:对 Array 方法的熟悉程度,还有逻辑算法能力。一般我会先让应聘者说出几种方法,然后随机抽取他说的一种,具体地写一下。
这里有一个通用的面试技巧:自己不熟悉的东西,千万别说!我就碰到过几个应聘者,想尽可能地表现自己,就说了不少方法,随机抽了一个,结果就没写出来,很尴尬。
ok,让我们马上开始今天的主题。会介绍 10 种不同类型的方法,一些类似的方法我做了合并,写法从简到繁,其中还会有 loadsh 源码中的方法。
10 种去重方法
假设有一个这样的数组: let originalArray = [1, '1', '1', 2, true, 'true', false, false, null, null, {}, {}, 'abc', 'abc', undefined, undefined, NaN, NaN]; 。后面的方法中的源数组,都是指的这个。
1、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。
2、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。
3、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。
4、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 。
5、双层 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 。
6、原始去重
定义一个新数组,并存放原数组的第一个元素,然后将源数组一一和新数组的元素对比,若不同则存放在新数组中。
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 。
7、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。
8、对象的属性
每次取出原数组的元素,然后在对象中访问这个属性,如果存在就说明重复。
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]
但这种方法有缺陷。从结果看,它貌似只关心值,不关注类型。还把 {} 给处理了,但这不是正统的处理办法,所以 不推荐使用 。
9、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]
这 貌似 是目前看来最完美的解决方案了。这里稍加解释一下:
typeof item + item obj[typeof item + item] = true
看似 完美解决了我们源数组的去重问题,但在实际的开发中,一般不会给两个空对象给我们去重。所以稍加改变源数组,给两个空对象中加入键值对。
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} 给去除了!!!这就不对了。
所以,这种方法有点去重 过头 了,也是存在问题的。
10、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:欢迎关注我的公众号 “超哥前端小栈”,交流更多的想法与技术。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 前端踩坑之数组拷贝
- 前端通关日记之优雅添加数组元素
- 前端算法题:二维数组中(每个一维数组的长度相同),左右和上下分别递增,求是否含有指定整数
- 一篇文章完全掌握 JavaScript 数组操作[每日前端夜话0x87]
- C语言指针数组和数组指针
- 数组 – 如何在Swift中将数组拆分成两半?
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Impractical Python Projects
Lee Vaughan / No Starch Press / 2018-11 / USD 29.95
Impractical Python Projects picks up where the complete beginner books leave off, expanding on existing concepts and introducing new tools that you’ll use every day. And to keep things interesting, ea......一起来看看 《Impractical Python Projects》 这本书的介绍吧!