媒介
这是前端口试题系列的第 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]
这 貌似
是现在看来最圆满的处置惩罚方案了。这里略加解释一下:
- 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}
给去除了!!!这就不对了。
所以,这类要领有点去重 过甚
了,也是存在题目的。
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:迎接关注我的民众号 “超哥前端小栈”,交换更多的主意与手艺。