摘要:后面的方法中的源數(shù)組,都是指的這個(gè)。它類似于數(shù)組,但是成員的值都是唯一的,沒有重復(fù)的值。這貌似是目前看來最完美的解決方案了。所以稍加改變?cè)磾?shù)組,給兩個(gè)空對(duì)象中加入鍵值對(duì)。
前言
這是前端面試題系列的第 8 篇,你可能錯(cuò)過了前面的篇章,可以在這里找到:
JavaScript 中的事件機(jī)制(從原生到框架)
理解函數(shù)的柯里化
ES6 中箭頭函數(shù)的用法
this 的原理以及用法
偽類與偽元素的區(qū)別及實(shí)戰(zhàn)
如何實(shí)現(xiàn)一個(gè)圣杯布局?
今日頭條 面試題和思路解析
前端面試中經(jīng)常會(huì)問到數(shù)組去重的問題。因?yàn)樵谄綍r(shí)的工作中遇到復(fù)雜交互的時(shí)候,需要知道該如何解決。另外,我在問應(yīng)聘者這道題的時(shí)候,更多的是想考察 2 個(gè)點(diǎn):對(duì) Array 方法的熟悉程度,還有邏輯算法能力。一般我會(huì)先讓應(yīng)聘者說出幾種方法,然后隨機(jī)抽取他說的一種,具體地寫一下。
這里有一個(gè)通用的面試技巧:自己不熟悉的東西,千萬別說!我就碰到過幾個(gè)應(yīng)聘者,想盡可能地表現(xiàn)自己,就說了不少方法,隨機(jī)抽了一個(gè),結(jié)果就沒寫出來,很尷尬。
ok,讓我們馬上開始今天的主題。會(huì)介紹 10 種不同類型的方法,一些類似的方法我做了合并,寫法從簡(jiǎn)到繁,其中還會(huì)有 loadsh 源碼中的方法。
10 種去重方法假設(shè)有一個(gè)這樣的數(shù)組: let originalArray = [1, "1", "1", 2, true, "true", false, false, null, null, {}, {}, "abc", "abc", undefined, undefined, NaN, NaN];。后面的方法中的源數(shù)組,都是指的這個(gè)。
1、ES6 的 Set 對(duì)象ES6 提供了新的數(shù)據(jù)結(jié)構(gòu) Set。它類似于數(shù)組,但是成員的值都是唯一的,沒有重復(fù)的值。Set 本身是一個(gè)構(gòu)造函數(shù),用來生成 Set 數(shù)據(jù)結(jié)構(gòu)。
let resultArr = Array.from(new Set(originalArray)); // 或者用擴(kuò)展運(yùn)算符 let resultArr = [...new Set(originalArray)]; console.log(resultArr); // [1, "1", 2, true, "true", false, null, {…}, {…}, "abc", undefined, NaN]
Set 并不是真正的數(shù)組,這里的 Array.from 和 ... 都可以將 Set 數(shù)據(jù)結(jié)構(gòu),轉(zhuǎn)換成最終的結(jié)果數(shù)組。
這是最簡(jiǎn)單快捷的去重方法,但是細(xì)心的同學(xué)會(huì)發(fā)現(xiàn),這里的 {} 沒有去重。可是又轉(zhuǎn)念一想,2 個(gè)空對(duì)象的地址并不相同,所以這里并沒有問題,結(jié)果 ok。
2、Map 的 has 方法把源數(shù)組的每一個(gè)元素作為 key 存到 Map 中。由于 Map 中不會(huì)出現(xiàn)相同的 key 值,所以最終得到的就是去重后的結(jié)果。
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 的數(shù)據(jù)結(jié)構(gòu)比較相似,結(jié)果 ok。
3、indexOf 和 includes建立一個(gè)新的空數(shù)組,遍歷源數(shù)組,往這個(gè)空數(shù)組里塞值,每次 push 之前,先判斷是否已有相同的值。
判斷的方法有 2 個(gè):indexOf 和 includes,但它們的結(jié)果之間有細(xì)微的差別。先看 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,結(jié)果 ok。
4、sort先將原數(shù)組排序,生成新的數(shù)組,然后遍歷排序后的數(shù)組,相鄰的兩兩進(jìn)行比較,如果不同則存入新數(shù)組。
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]
從結(jié)果可以看出,對(duì)源數(shù)組進(jìn)行了排序。但同樣的沒有處理 NaN。
5、雙層 for 循環(huán) + splice雙層循環(huán),外層遍歷源數(shù)組,內(nèi)層從 i+1 開始遍歷比較,相同時(shí)刪除這個(gè)值。
for (let i = 0; i < originalArray.length; i++) { for (let j = (i + 1); j < originalArray.length; j++) { // 第一個(gè)等于第二個(gè),splice去掉第二個(gè) 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 方法會(huì)修改源數(shù)組,所以這里我們并沒有新開空數(shù)組去存儲(chǔ),最終輸出的是修改之后的源數(shù)組。但同樣的沒有處理 NaN。
6、原始去重定義一個(gè)新數(shù)組,并存放原數(shù)組的第一個(gè)元素,然后將源數(shù)組一一和新數(shù)組的元素對(duì)比,若不同則存放在新數(shù)組中。
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 的第一個(gè)參數(shù)是一個(gè) callback,callback 中的參數(shù)分別為: Accumulator(累加器)、currentValue(當(dāng)前正在處理的元素)、currentIndex(當(dāng)前正在處理的元素索引,可選)、array(調(diào)用 reduce 的數(shù)組,可選)。
reduce 的第二個(gè)參數(shù),是作為第一次調(diào)用 callback 函數(shù)時(shí)的第一個(gè)參數(shù)的值。如果沒有提供初始值,則將使用數(shù)組中的第一個(gè)元素。
利用 reduce 的特性,再結(jié)合之前的 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 是累加器,在這里的作用是將沒有重復(fù)的值塞入新數(shù)組(它一開始是空的)。 reduce 的寫法很簡(jiǎn)單,但需要多加理解。它可以處理 NaN,結(jié)果 ok。
8、對(duì)象的屬性每次取出原數(shù)組的元素,然后在對(duì)象中訪問這個(gè)屬性,如果存在就說明重復(fù)。
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]
但這種方法有缺陷。從結(jié)果看,它貌似只關(guān)心值,不關(guān)注類型。還把 {} 給處理了,但這不是正統(tǒng)的處理辦法,所以 不推薦使用。
9、filter + hasOwnPropertyfilter 方法會(huì)返回一個(gè)新的數(shù)組,新數(shù)組中的元素,通過 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 方法會(huì)返回一個(gè)布爾值,指示對(duì)象自身屬性中是否具有指定的屬性。
typeof item + item 的寫法,是為了保證值相同,但類型不同的元素被保留下來。例如:第一個(gè)元素為 number1,第二第三個(gè)元素都是 string1,所以第三個(gè)元素就被去除了。
obj[typeof item + item] = true 如果 hasOwnProperty 沒有找到該屬性,則往 obj 里塞鍵值對(duì)進(jìn)去,以此作為下次循環(huán)的判斷依據(jù)。
如果 hasOwnProperty 沒有檢測(cè)到重復(fù)的屬性,則告訴 filter 方法可以先積攢著,最后一起輸出。
看似 完美解決了我們?cè)磾?shù)組的去重問題,但在實(shí)際的開發(fā)中,一般不會(huì)給兩個(gè)空對(duì)象給我們?nèi)ブ亍K陨约痈淖冊(cè)磾?shù)組,給兩個(gè)空對(duì)象中加入鍵值對(duì)。
let originalArray = [1, "1", "1", 2, true, "true", false, false, null, null, {a: 1}, {a: 2}, "abc", "abc", undefined, undefined, NaN, NaN];
然后再用 filter + hasOwnProperty 去重。
然而,結(jié)果竟然把 {a: 2} 給去除了!!!這就不對(duì)了。
所以,這種方法有點(diǎn)去重 過頭 了,也是存在問題的。
10、lodash 中的 _.uniq靈機(jī)一動(dòng),讓我想到了 lodash 的去重方法 _.uniq,那就嘗試一把:
console.log(_.uniq(originalArray)); // [1, "1", 2, true, "true", false, null, {…}, {…}, "abc", undefined, NaN]
用法很簡(jiǎn)單,可以在實(shí)際工作中正確處理去重問題。
然后,我在好奇心促使下,看了它的源碼,指向了 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 }
有比較多的干擾項(xiàng),那是為了兼容另外兩個(gè)方法,_.uniqBy 和 _.uniqWith。去除掉之后,就會(huì)更容易發(fā)現(xiàn)它是用 while 做了循環(huán)。當(dāng)遇到相同的值得時(shí)候,continue outer 再次進(jìn)入循環(huán)進(jìn)行比較,將沒有重復(fù)的值塞進(jìn) result 里,最終輸出。
另外,_.uniqBy 方法可以通過指定 key,來專門去重對(duì)象列表。
_.uniqBy([{ "x": 1 }, { "x": 2 }, { "x": 1 }], "x"); // => [{ "x": 1 }, { "x": 2 }]
_.uniqWith 方法可以完全地給對(duì)象中所有的鍵值對(duì),進(jìn)行比較。
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 }]
這兩個(gè)方法,都還挺實(shí)用的。
總結(jié)從上述的這些方法來看,ES6 開始出現(xiàn)的方法(如 Set、Map、includes),都能完美地解決我們?nèi)粘i_發(fā)中的去重需求,關(guān)鍵它們還都是原生的,寫法還更簡(jiǎn)單。
所以,我們提倡擁抱原生,因?yàn)樗鼈冋娴臎]有那么難以理解,至少在這里我覺得它比 lodash 里 _.uniq 的源碼要好理解得多,關(guān)鍵是還能解決問題。
PS:歡迎關(guān)注我的公眾號(hào) “超哥前端小棧”,交流更多的想法與技術(shù)。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.specialneedsforspecialkids.com/yun/102332.html
摘要:深拷貝與淺拷貝的出現(xiàn),就與這兩個(gè)數(shù)據(jù)類型有關(guān)。這時(shí),就需要用淺拷貝來實(shí)現(xiàn)了。數(shù)據(jù)一但過多,就會(huì)有遞歸爆棧的風(fēng)險(xiǎn)。這個(gè)方法是在解決遞歸爆棧問題的基礎(chǔ)上,加以改進(jìn)解決循環(huán)引用的問題。但如果你并不想保持引用,那就改用用于解決遞歸爆棧即可。 前言 這是前端面試題系列的第 9 篇,你可能錯(cuò)過了前面的篇章,可以在這里找到: 數(shù)組去重(10 種濃縮版) JavaScript 中的事件機(jī)制(從原生到...
摘要:今天要講的,是我從的源碼實(shí)現(xiàn)文件中學(xué)到的幾個(gè)很基礎(chǔ),卻又容易被忽略的知識(shí)點(diǎn)。在函數(shù)式編程中,函數(shù)是一等公民,它可以只是根據(jù)參數(shù),做簡(jiǎn)單的組合操作,再作為別的函數(shù)的返回值。所以,閱讀源碼,是一種很棒的重溫基礎(chǔ)知識(shí)的方式。 showImg(https://segmentfault.com/img/bVbpTSY?w=750&h=422); 前言 上一篇文章 「前端面試題系列8」數(shù)組去重(1...
摘要:封裝手寫的方筆記使用檢測(cè)文件前端掘金副標(biāo)題可以做什么以及使用中會(huì)遇到的坑。目的是幫助人們用純中文指南實(shí)現(xiàn)復(fù)選框中多選功能前端掘金作者緝熙簡(jiǎn)介是推出的一個(gè)天挑戰(zhàn)。 深入理解 JavaScript Errors 和 Stack Traces - 前端 - 掘金譯者注:本文作者是著名 JavaScript BDD 測(cè)試框架 Chai.js 源碼貢獻(xiàn)者之一,Chai.js 中會(huì)遇到很多異常處理...
摘要:封裝手寫的方筆記使用檢測(cè)文件前端掘金副標(biāo)題可以做什么以及使用中會(huì)遇到的坑。目的是幫助人們用純中文指南實(shí)現(xiàn)復(fù)選框中多選功能前端掘金作者緝熙簡(jiǎn)介是推出的一個(gè)天挑戰(zhàn)。 深入理解 JavaScript Errors 和 Stack Traces - 前端 - 掘金譯者注:本文作者是著名 JavaScript BDD 測(cè)試框架 Chai.js 源碼貢獻(xiàn)者之一,Chai.js 中會(huì)遇到很多異常處理...
摘要:設(shè)計(jì)模式是以面向?qū)ο缶幊虨榛A(chǔ)的,的面向?qū)ο缶幊毯蛡鹘y(tǒng)的的面向?qū)ο缶幊逃行┎顒e,這讓我一開始接觸的時(shí)候感到十分痛苦,但是這只能靠自己慢慢積累慢慢思考。想繼續(xù)了解設(shè)計(jì)模式必須要先搞懂面向?qū)ο缶幊蹋駝t只會(huì)讓你自己更痛苦。 JavaScript 中的構(gòu)造函數(shù) 學(xué)習(xí)總結(jié)。知識(shí)只有分享才有存在的意義。 是時(shí)候替換你的 for 循環(huán)大法了~ 《小分享》JavaScript中數(shù)組的那些迭代方法~ ...
閱讀 1476·2023-04-26 00:08
閱讀 809·2021-11-23 18:51
閱讀 1683·2021-11-12 10:34
閱讀 1018·2021-10-14 09:43
閱讀 509·2021-08-18 10:23
閱讀 2589·2019-08-30 15:55
閱讀 3399·2019-08-30 11:05
閱讀 2799·2019-08-29 12:50