JavaScript開發中數組加工極爲常見,其次在面試中被問及的機率也特別高,一直想整理一篇關於數組常見操做的文章,本文也算了卻心願了。javascript
說在前面,文中的實現並不是最佳,實現雖然有不少種,但我以爲你們至少應該掌握一種,這樣在面試能解決大部分數組問題。在瞭解實現思路後,平常開發中結合實際場景優化實現,提高性能也是後期該考慮的。html
本文主要圍繞數組去重、數組排序、數組降維、數組合並、數組過濾、數組求差集,並集,交集,數組是否包含某項等知識點展開,附帶部分知識拓展,在看實現代碼前也建議你們先自行思考,那麼本文開始。java
數組去重我分爲兩種狀況,簡單數組去重與對象數組去重。所謂簡單數組即元素均爲基本數據類型,以下:面試
let arr = [undefined, 0, 1, 2, 2, 3, 4, 0, undefined]; let arr_ = arr.filter((self, index, arr) => index === arr.indexOf(self)); console.log(arr_); //[undefined, 0, 1, 2, 3, 4]
有沒有更簡單的作法?有的同窗確定想到了ES6新增的Set數據結構,這也是去重的妙招,原理是Set結構不接受重複值,以下:算法
[...new Set([undefined, 0, 1, 2, 2, 3, 4, 0, undefined])]//[undefined, 0, 1, 2, 3, 4]
對象數組顧名思義,每一個元素都是一個對象,好比咱們但願去除掉name
屬性相同的對象:數組
let arr = [{name:'echo'},{name:'聽風是風'},{name:'echo'},{name:'時間跳躍'}]; let keys = {}; let arr_ = arr.reduce((accumulator,currentValue)=>{ !keys[currentValue['name']] ? keys[currentValue['name']] = true && accumulator.push(currentValue) : null; return accumulator; },[]); console.log(arr_);//[{name:'echo'},{name:'聽風是風'},{name:'時間跳躍'}]
思路並不難,咱們藉助一個空對象keys
,將每次出現過的對象的name值做爲key,並將其設置爲true
;那麼下次出現時根據三元判斷天然會跳過push
操做,從而達到去重目的。瀏覽器
reduce存在必定兼容問題,至少徹底不兼容IE,不過咱們知道了這個思路,即便使用forEach
一樣能作到上面的效果,改寫就留給你們了。數據結構
有同窗確定就想到了,能不能使用Set去重對象數組呢?其實並不能,由於對於JavaScript來講,兩個長得相同的對象只是外觀相同,它們的引用地址並不一樣,好比:app
[1,2,3]===[1,2,3]//false
因此對於Set結構而言,它們就是不一樣的兩個值,好比下面這個例子:函數
[...new Set([{name:'echo'},{name:'echo'}])]//{name:'echo'},{name:'echo'}
淺拷貝可讓兩個對象徹底相等,以下:
let a=[1,2]; let b = a; console.log(a===b);//true
因此咱們能夠用new Set()去重引用地址相同的對象:
let a = {name:'echo'}; let b = a; console.log([...new Set([a,b])]); //{name: "echo"}
大概這麼個意思,關於數組去重先說到這。
數組降維什麼意思?舉個例子,將二維數組[[1,2],[3,4]]
轉變爲一維數組[1,2,3,4 ]
。
ES6中新增了數組降維方法flat
,使用比較簡單,好比就上面的例子能夠這麼作:
let arr = [[1,2],[3,4]]; let arr_ = arr.flat(); console.log(arr_);//[1, 2, 3, 4]
若是是三維數組怎麼辦呢?falt
方法接受一個參數表示降維的層數,默認爲1,你能夠理解爲要去掉 [] 的層數。
三維數組降維能夠這麼寫:
let arr = [[1,2],[3,4],[5,[6]]]; let arr_ = arr.flat(2); console.log(arr_);//[1, 2, 3, 4, 5, 6]
若是你不知道數組要降維的層數,你能夠直接將參數設置爲infinity
(無限大),這樣無論你是幾維都會被降爲一維數組:
let arr = [[[[[1,2]]]]]; let arr_ = arr.flat(Infinity); console.log(arr_);//[1, 2]
簡單粗暴,好用是好用,兼容也是個大問題,谷歌版本從69才徹底支持,其它瀏覽器天然沒得說。
咱們能夠簡單模擬flat實現,以下:
let arr = [0, [1], [2, 3], [4, [5, 6, 7]] ]; function flat_(arr) { if (!Array.isArray(arr)) { throw new Error('The argument must be an array.'); }; let arr_ = []; arr.forEach((self) => { Array.isArray(self) ? arr_.push.apply(arr_, flat_(self)) : arr_.push(self); }); return arr_; }; flat_(arr); //[0, 1, 2, 3, 4, 5, 6, 7]
在這個實現中,巧妙使用apply
參數接受數組的特色,讓push
也能扁平化接受一個一維數組,從而達到數組合並的目的。
換種思路,使用reduce
結合concat
方法,實現能夠更簡單一點點,以下:
function flat_(arr) { if (!Array.isArray(arr)) { throw new Error('The argument must be an array.'); }; return arr.reduce((accumulator, currentValue) => { return accumulator.concat(Array.isArray(currentValue) ? flat_(currentValue) : currentValue); }, []); }; console.log(flat_(arr));//[0, 1, 2, 3, 4, 5, 6, 7]
這個實現也只是省略了建立新數組與返回新數組兩行代碼,這兩個操做reduce都幫咱們作了。
實現一依賴的是push,實現二依賴的是concat,同爲數組方法,這裏說幾個你們容易忽略的知識點。
concat除了能合併數組,其實也能合併簡單類型數據,實現二中正是利用了這一點:
[1,2,3].concat([4]);//[1,2,3,4] [1,2,3].concat(4);//[1,2,3,4]
concat返回合併後的新數組,而push返回添加操做後數組的長度
let a = [1,2,3].concat([4]); console.log(a);//[1,2,3,4] let b = [1,2,3].push(4); console.log(b);//4
concat屬於淺拷貝,這是不少人都容易誤解的一個點,一個誤解的例子:
let arr = [1,2,3]; let a = arr.concat(); arr[0] = 0; console.log(a);//[1, 2, 3]
而在下面這個例子中,你會發現concat確實是淺拷貝:
let arr_ = [[1,2],[3]]; let a_ = arr_.concat(); arr_[0][0] = 0; console.log(a_);//[[0,2],[3]]
這是爲何?在MDN文檔說明中解釋的很清楚,concat建立一個新數組,新數組由被調用的數組元素組成,且元素順序與原數組保持一致。元素複製操做中分爲基本類型與引用類型兩種狀況:
數據類型如字符串,數字和布爾(不是
String
,Number
和Boolean
對象):concat
將字符串和數字的值複製到新數組中。
對象引用(而不是實際對象):
concat
將對象引用複製到新數組中。 原始數組和新數組都引用相同的對象。 也就是說,若是引用的對象被修改,則更改對於新數組和原始數組都是可見的。 這包括也是數組的數組參數的元素。
有人以爲concat是深拷貝,也是由於數組中的元素剛好是基本數據類型,這點但願你們謹記。那麼關於數組降維就說到這裏了。
在介紹數組降維時咱們順帶說起了數組合並的一些作法,若是隻是合併兩個數組咱們能夠這樣作:
let arr1 = [1, 2]; let arr2 = [3, 4]; arr1.concat(arr2); //[1,2,3,4] arr1.push.apply(arr1, arr2); arr1; //[1,2,3,4] Array.prototype.concat.apply(arr1, arr2); //[1,2,3,4]
那若是是未知個數的數組須要合併怎麼作呢?使用ES6寫法很是簡單:
let arr1 = [1, 2], arr2 = [3, 4], arr3 = [5, 6]; function concat_(...rest) { return [...rest].flat(); }; concat_(arr1, arr2, arr3); //[1, 2, 3, 4, 5, 6]
這裏一共只作了兩件事,使用函數rest參數配合拓展運算符...將三個數組組成成一個二維數組,再利用flat降維。
固然考慮兼容問題,咱們能夠保守一點這麼去寫:
let arr1 = [1, 2], arr2 = [3, 4], arr3 = [5, 6]; function concat_() { let arr_ = Array.prototype.slice.call(arguments); let result = []; arr_.forEach(self => { result.push.apply(result, self); }); return result; }; concat_(arr1, arr2, arr3); //[1, 2, 3, 4, 5, 6]
有同窗必定在想,爲何forEach
內不直接使用result.concat(self)
解決合併呢?緣由有兩點:
concat不修改原數組而是返回一個新數組,因此循環屢次result仍是空數組。
forEach不支持return,沒法將合併過的數組返回供下次繼續合併,這兩個問題使用reduce都能解決。
這個天然不用說了,我想你們首先想到的天然是sort排序,直接上代碼:
//升序 [1, 0, 2, 5, 4, 3].sort((a, b) => a - b); //[0,1,2,3,4,5] //降序 [1, 0, 2, 5, 4, 3].sort((a, b) => b - a); //[5,4,3,2,1,0]
那麼問題就來了,雖然咱們知道sort是按字符編碼的順序進行排序,那麼上述代碼中的回調函數起到了什麼做用?其實這一點在JavaScript權威指南中給出了答案:
若想讓sort按照其它方式而非字母表順序進行數組排序,必須給sort方法傳遞一個比較函數。該函數決定了它的兩個參數在排好序的數組中的前後順序,假設第一個參數應該在前,比較函數應該返回一個小於0的數值;相反,假設第一個參數應該在後,函數應該返回一個大於0的數值。而且,假設兩個值相等,函數應該返回0;
什麼意思呢?以上面的a - b
爲例,由於ab均爲數字,因此計算結果只能是正數,0,負數三種狀況,若是爲負數則a排在b前面,若是相等,ab順序不變,若是爲正數,a排在b後面,大概這個意思。
咱們將問題升級,如今須要按照年齡從小到大對用戶進行排序,能夠這麼作:
var arr = [{ name: 'echo', age: 18 }, { name: '聽風是風', age: 26 }, { name: '時間跳躍', age: 10 }, { name: '行星飛行', age: 16 }]; arr.sort((a, b) => { var a_ = a.age; var b_ = b.age; return a_ - b_; });
比較巧的是上面2個例子參與比較的元素都爲數字,因此能參與計算比較,前面已經說了sort方法默認是按照字符編碼的順序進行排序:
['c', 'b', 'a', 'e', 'd'].sort();//["a", "b", "c", "d", "e"]
如今要求以上字母按z-a倒序排列,怎麼作?雖然字母沒法計算,但仍是有大小之分,仍是同樣的作法,以下:
['c', 'b', 'a', 'e', 'd'].sort((a, b) => { let result; if (a < b) { result = 1; } else if (a > b) { result = -1; } else { result = 0; }; return result; }); //["e", "d", "c", "b", "a"]
在介紹sort回調含義的時候已有解釋,若但願從小到大排列,a<b應該返回小於0的數字,但咱們但願排序是由大到小,因此反過來就能夠了,讓a<b時返回大於0的數字,a>b返回小於0的數字,這樣就能夠實現倒序排列。
我知道,關於排序你們都有聽過冒泡、插入等十大經典排序算法,由於篇幅問題這裏就不貼代碼了,若是時間容許我會專門寫一篇簡單易懂的十大排序的文章,那麼關於排序就說到這裏了。
數組過濾在開發中即爲常見,咱們通常遇到兩種狀況,一是將符合條件的元素篩選出來,包含在一個新數組中供後續使用;二是將符合條件的元素從原數組中剔除。
咱們先說說第一種狀況,篩選符合條件的元素,實現不少種,首推filter,正如單詞含義同樣用於過濾:
// 篩選3的倍數 [1, 2, 3, 4, 5, 6, 7, 8, 9].filter(self => self % 3 === 0);//[3,6,9]
第二種刪除符合條件的元素,這裏可使用for循環:
// 剔除3的倍數 let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9], i = 0, length = arr.length; for (; i < length; i++) { // 刪除數組中全部的1 if (arr[i] % 3 === 0) { arr.splice(i, 1); //重置i,不然i會跳一位 i--; }; }; console.log(arr);//[1, 2, 4, 5, 7, 8]
咱們換種思路,剔除數組中3的倍數不就是在找不是3的倍數的元素嗎,因此仍是可使用filter作到這一點:
[1, 2, 3, 4, 5, 6, 7, 8, 9].filter(self => !(self % 3 === 0));
有同窗確定納悶爲何不用forEach作呢?這是由於forEach不像for循環能重置i同樣重置index,其次不像filter能return數據,對於forEach使用更多細節能夠閱讀博主這篇文章 forEach參數詳解,forEach與for循環區別 。那麼關於數組過濾就說到這裏了。
同爲高頻操做,不少同窗習慣使用for或者forEach用來作此操做,其實相比之下,find與some方法更爲實現,先看find:
var result = ['echo', '聽風是風', '時間跳躍', '聽風是風'].find((self) => { console.log(1);//執行2次 return self === '聽風是風' }); console.log(result); //聽風是風
再看some方法:
var result = ['echo', '聽風是風', '時間跳躍'].some((self) => { console.log(1);//執行2次 return self === '聽風是風' }); console.log(result); //true
find方法返回第一個符合條件的目標元素,並跳出循環,而some只要找到有一個符合條件則返回布爾值true。二者都自帶跳出循環機制,相比for循環使用break以及forEach沒法break更加方便,特別是some的返回結果更利於後面的條件判斷邏輯。
另外ES6數組新增了簡單粗暴的includes方法,能直接用於判斷數組是否包含某元素,最大亮點就是能判斷是否包含NaN,畢竟你們都知道NaN是惟一不等於本身的特殊存在。
[1,2,3,NaN].includes(NaN);//true
includes方法徹底不兼容IE,這裏只是順帶一提,實際開發中還得謹慎使用。
在說實現以前,咱們簡單複習數學中關於並集,交集與差集的概念。
假設如今有數組A [1,2,3]與數組B [3,4,5],由於3在兩個數組中均有出現,因此3是數組AB的交集。
那麼對應的數字1,2只在A中存在,4,5只在B中出現,因此1,2,3,4屬於AB的共同差集。
而並集則是指分別出如今AB中的全部數字,但不記重複,因此是1,2,3,4,5,注意只有一個3。
在瞭解基本概念後,咱們先說說如何作到求並集;聰明的同窗立刻就想到了並集等於數組合並加去重:
//ES6 求並集 function union(a, b) { return a.concat(b).filter((self, index, arr) => index === arr.indexOf(self)); }; console.log(union([1, 2, 3], [3, 4, 5])); //[1,2,3,4,5]
固然使用存在兼容性的ES6會更簡單:
//ES6 求並集 function union(a, b) { return Array.from(new Set([...a, ...b])); }; console.log(union([1, 2, 3], [3, 4, 5])); //[1,2,3,4,5]
咱們再來講說數組求交集,即元素同時存在兩個數組中,由於太困了,這裏我偷個懶使用了includes方法:
function intersect(a, b) { return a.filter(self => { return b.includes(self); }); }; console.log(intersect([1, 2, 3], [3, 4, 5]));//[3]
差集就好說了,在上方代碼中includes前加個!便可,這裏作個演示只求b數組的差集:
function difference (a, b) { return a.filter(self => { return !(b.includes(self)); }); }; console.log(difference ([1, 2, 3], [3, 4, 5])); //[1, 2]
那麼到這裏,咱們藉着彙總數組常見操做的契機,複習了數組常見API與部分容易忽略的知識。對於數組去重,降維,排序等操做都至少給出了一種解決思路。如有對於文中實現有更好的建議或疑問,也歡迎你們留言。我會在第一時間回覆。另外,撕帶油的遊戲必定要當心當心再當心,否則就會像我這樣毀掉一件衣服。
那麼本文到這裏就結束了,我是真的好睏好睏,我還沒買到回家的票!!!!含淚睡覺。