這是前端面試題系列的第 8 篇,你可能錯過了前面的篇章,能夠在這裏找到:前端
前端面試中常常會問到數組去重的問題。由於在平時的工做中遇到複雜交互的時候,須要知道該如何解決。另外,我在問應聘者這道題的時候,更多的是想考察 2 個點:對 Array 方法的熟悉程度,還有邏輯算法能力。通常我會先讓應聘者說出幾種方法,而後隨機抽取他說的一種,具體地寫一下。面試
這裏有一個通用的面試技巧:本身不熟悉的東西,千萬別說!我就碰到過幾個應聘者,想盡量地表現本身,就說了很多方法,隨機抽了一個,結果就沒寫出來,很尷尬。算法
ok,讓咱們立刻開始今天的主題。會介紹 10 種不一樣類型的方法,一些相似的方法我作了合併,寫法從簡到繁,其中還會有 loadsh 源碼中的方法。數組
假設有一個這樣的數組: let originalArray = [1, '1', '1', 2, true, 'true', false, false, null, null, {}, {}, 'abc', 'abc', undefined, undefined, NaN, NaN];
。後面的方法中的源數組,都是指的這個。bash
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。函數
把源數組的每個元素做爲 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。post
創建一個新的空數組,遍歷源數組,往這個空數組裏塞值,每次 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。
先將原數組排序,生成新的數組,而後遍歷排序後的數組,相鄰的兩兩進行比較,若是不一樣則存入新數組。
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
。
雙層循環,外層遍歷源數組,內層從 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
。
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 來檢查是否爲符合條件的元素。
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
的寫法,是爲了保證值相同,但類型不一樣的元素被保留下來。例如:第一個元素爲 number1,第二第三個元素都是 string1,因此第三個元素就被去除了。obj[typeof item + item] = true
若是 hasOwnProperty 沒有找到該屬性,則往 obj 裏塞鍵值對進去,以此做爲下次循環的判斷依據。看似
完美解決了咱們源數組的去重問題,但在實際的開發中,通常不會給兩個空對象給咱們去重。因此稍加改變源數組,給兩個空對象中加入鍵值對。
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,那就嘗試一把:
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:歡迎關注個人公衆號 「超哥前端小棧」,交流更多的想法與技術。