
壹 ❀ 引
JavaScript開發中數組加工極為常見,其次在面試中被問及的概率也特別高,一直想整理一篇關於數組常見操作的文章,本文也算了卻心願了。
說在前面,文中的實現並非最佳,實現雖然有很多種,但我覺得大家至少應該掌握一種,這樣在面試能解決大部分數組問題。在了解實現思路后,日常開發中結合實際場景優化實現,提升性能也是后期該考慮的。
本文主要圍繞數組去重、數組排序、數組降維、數組合並、數組過濾、數組求差集,並集,交集,數組是否包含某項等知識點展開,附帶部分知識拓展,在看實現代碼前也建議大家先自行思考,那么本文開始。
貳 ❀ 常見數組操作
貳 ❀ 壹 數組去重
數組去重我分為兩種情況,簡單數組去重與對象數組去重。所謂簡單數組即元素均為基本數據類型,如下:
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來說,兩個長得相同的對象只是外觀相同,它們的引用地址並不同,比如:
[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與部分容易忽略的知識。對於數組去重,降維,排序等操作都至少給出了一種解決思路。若有對於文中實現有更好的建議或疑問,也歡迎大家留言。我會在第一時間回復。另外,撕帶油的東西一定要小心小心再小心,不然就會像我這樣毀掉一件衣服。

那么本文到這里就結束了,我是真的好困好困,我還沒買到回家的票!!!!含淚睡覺。