摘要:后面的方法中的源數組,都是指的這個。它類似于數組,但是成員的值都是唯一的,沒有重復的值。這貌似是目前看來最完美的解決方案了。所以稍加改變源數組,給兩個空對象中加入鍵值對。
前言
這是前端面試題系列的第 8 篇,你可能錯過了前面的篇章,可以在這里找到:
JavaScript 中的事件機制(從原生到框架)
理解函數的柯里化
ES6 中箭頭函數的用法
this 的原理以及用法
偽類與偽元素的區別及實戰
如何實現一個圣杯布局?
今日頭條 面試題和思路解析
前端面試中經常會問到數組去重的問題。因為在平時的工作中遇到復雜交互的時候,需要知道該如何解決。另外,我在問應聘者這道題的時候,更多的是想考察 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 的 reducereduce 是 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 + hasOwnPropertyfilter 方法會返回一個新的數組,新數組中的元素,通過 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:歡迎關注我的公眾號 “超哥前端小棧”,交流更多的想法與技術。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/102332.html
摘要:深拷貝與淺拷貝的出現,就與這兩個數據類型有關。這時,就需要用淺拷貝來實現了。數據一但過多,就會有遞歸爆棧的風險。這個方法是在解決遞歸爆棧問題的基礎上,加以改進解決循環引用的問題。但如果你并不想保持引用,那就改用用于解決遞歸爆棧即可。 前言 這是前端面試題系列的第 9 篇,你可能錯過了前面的篇章,可以在這里找到: 數組去重(10 種濃縮版) JavaScript 中的事件機制(從原生到...
摘要:今天要講的,是我從的源碼實現文件中學到的幾個很基礎,卻又容易被忽略的知識點。在函數式編程中,函數是一等公民,它可以只是根據參數,做簡單的組合操作,再作為別的函數的返回值。所以,閱讀源碼,是一種很棒的重溫基礎知識的方式。 showImg(https://segmentfault.com/img/bVbpTSY?w=750&h=422); 前言 上一篇文章 「前端面試題系列8」數組去重(1...
摘要:封裝手寫的方筆記使用檢測文件前端掘金副標題可以做什么以及使用中會遇到的坑。目的是幫助人們用純中文指南實現復選框中多選功能前端掘金作者緝熙簡介是推出的一個天挑戰。 深入理解 JavaScript Errors 和 Stack Traces - 前端 - 掘金譯者注:本文作者是著名 JavaScript BDD 測試框架 Chai.js 源碼貢獻者之一,Chai.js 中會遇到很多異常處理...
摘要:封裝手寫的方筆記使用檢測文件前端掘金副標題可以做什么以及使用中會遇到的坑。目的是幫助人們用純中文指南實現復選框中多選功能前端掘金作者緝熙簡介是推出的一個天挑戰。 深入理解 JavaScript Errors 和 Stack Traces - 前端 - 掘金譯者注:本文作者是著名 JavaScript BDD 測試框架 Chai.js 源碼貢獻者之一,Chai.js 中會遇到很多異常處理...
摘要:設計模式是以面向對象編程為基礎的,的面向對象編程和傳統的的面向對象編程有些差別,這讓我一開始接觸的時候感到十分痛苦,但是這只能靠自己慢慢積累慢慢思考。想繼續了解設計模式必須要先搞懂面向對象編程,否則只會讓你自己更痛苦。 JavaScript 中的構造函數 學習總結。知識只有分享才有存在的意義。 是時候替換你的 for 循環大法了~ 《小分享》JavaScript中數組的那些迭代方法~ ...
閱讀 1467·2023-04-26 00:08
閱讀 797·2021-11-23 18:51
閱讀 1672·2021-11-12 10:34
閱讀 1008·2021-10-14 09:43
閱讀 502·2021-08-18 10:23
閱讀 2581·2019-08-30 15:55
閱讀 3392·2019-08-30 11:05
閱讀 2792·2019-08-29 12:50