目的
在平常的前端開發中,一般需要處理資料(陣列和物件居多),特別是複雜功能的頁面,通常是一到兩個物件陣列(有時陣列裡面還有陣列)。大多數前端開發的難點就是這裡,耗時大。以前我在工作中,遇到的支付方式功能,排課日曆,場地預約,公園大螢幕運動排行和彈幕,後臺系統的許可權模組等等,這些功能難度大費腦耗時間多。其他那些功能很簡單的,無腦複製貼上執行就完成了。如果我可以總結一下,找出一些高效的處理陣列和物件的方法,以後工作中就對號入座的使用,這肯定可以提高我的工作效率,到時候我可以多出時間來給測試或者學習新知識或者摸魚休息。這總比每天瞎用js
工具類,碌碌無為的寫程式碼強。
做一件事有很多種方法,最快捷的就那一兩個,把它們找出來,事半功倍。正所謂磨刀不誤砍柴工。
陣列的遍歷
一、for...of(通用)
最好用的迭代(迴圈)語句
說到迴圈,javascript的迴圈語句那麼多,我該如何選擇?其實就是問下自己,自己需要什麼?我眼睛一閉,我要可以用在很多地方(物件),寫法簡單,這樣維護性高;我還要支援continue,break,throw,return
打斷迴圈或者跳到下一個迴圈(某一次迭代不執行迴圈體程式碼塊),我還要index
,index
等於某個值時做一些邏輯判斷。
for...of
,它可以用於可迭代物件。Array,Map,Set,String,TypeArray,arguments
等等。就是說可用的地方很廣,哪裡都能用。迭代是啥意思,就是說按順序訪問列表中的每一項,就是小時候老師家訪那樣,學生的家走一遍。其實在專案中,一般都是遍歷物件陣列,這樣的資料結構前端後臺耳熟能祥。
這個東西效能不算好的,是es2015推出來的,寫法特別簡潔。執行效率中下,比for,forEach
差,但是比map
好。優點是佔用記憶體最小。其實執行效率和佔用記憶體是次要的,因為在專案中,處理的資料的長度不會很長,一般在10到30之間,影響不大,再說前端在大多數情況下不需要考慮效能(上一家公司技術大哥給我的經驗)。寫法簡潔的話,在二層迴圈的情況程式碼的閱讀性和維護性就較高。
處理邏輯時,一般需要(特別需要)使用break,continue,return
來設計程式碼邏輯。不是所有的迴圈語句都支援return ,break
。記得有一次我使用forEach
語句的時候,迴圈體裡面加了break
,發現跳不出去。搞半天原來是不支援。現實又狠狠的打了我這種渣渣一耳光。改為for
語句解決了。
index
它是不支援的,不是你要什麼它就是支援什麼。可以通過把陣列等轉化為Map
物件。實現如下:
for(let [index,item] of new Map(targetArray.map((item,i) => [i,item]))){
console.log(index, item)
}
二、reduce,reduceRight(百變)
reduce
迴圈(招式很多)
一開始我以為reduce
只能求和,沒什麼用,查詢大神寫的部落格,我才知道它很厲害。我開始要抄別人的東西了。沒辦法,技術不行。
陣列求和,求乘積(其實求減,求除也是沒有問題的囉)
var arr = [1, 2, 3, 4];
var sum = arr.reduce((x,y)=>x+y)
var mul = arr.reduce((x,y)=>x*y)
console.log( sum ); //求和,10
console.log( mul ); //求乘積,24
//其實物件陣列也可以求和,為所欲為啊
var arr = [{num: 1}, {num: 2}];
var sum = arr.reduce((x,y)=>x+y.num,0)
計算一個陣列中元素成員的次數(利用了一個reduce
的第二引數初始化為空物件,用這個物件來記錄陣列出現的次數。然後把這個物件給餅狀圖。不過一般後臺會直接給前臺這個物件,不需要前臺計算。)
let names = ['Alice', 'Bob', 'Alice'];
let nameNum = names.reduce((pre,cur)=>{
if(cur in pre){
pre[cur]++
}else{
pre[cur] = 1
}
return pre
},{})
console.log(nameNum); //{Alice:
//萬一陣列的每一項是物件,能不能搞啊 可以的,擴充一下就可以了。看
let names = [{name:'Alice'}, {name: 'Bob'}, {name: 'Alice'}];
let nameNum = names.reduce((pre,cur)=>{
if(cur.name && cur.name in pre){
pre[cur.name]++
}else{
pre[cur.name] = 1
}
return pre
},{})
console.log(nameNum); //{Alice:
陣列去重,這個功能經常遇到,當陣列出現重複,後臺不好處理時,這時需要前端去重(利用了第二個引數初始化為空陣列,)
let arr = [1,2,3,4,4,1]
let newArr = arr.reduce((pre,cur)=>{
if(!pre.includes(cur)){
return pre.concat(cur)
}else{
return pre
}
},[])
console.log(newArr);// [1, 2, 3, 4]
//物件陣列去重和上面一樣,擴充一下
let arr = [{num: 1},{num: 2},{num: 3},{num: 3}]
let temp = {};
let newArr = arr.reduce((pre,cur)=>{
if(!temp[cur.num]){
temp[cur.num] = true;
return pre.concat(cur)
}else{
return pre
}
},[])
console.log(newArr);// [1, 2, 3, 4]
看了一下人家的部落格,發現reduce
還能快速實現filter
的功能。為所欲為啊。實現如下。
const data = [
{name: 'a', age: 37, weight: 72, sex: 'male'},
];
// accu 為 accumulator,curr 為 currentValue
const result = data.reduce((accu, curr) => {
// if 判斷,相當於 filter
if (curr.sex === 'female') {
accu.push(curr);
}
return accu;
}, []);
console.log(result);
遞迴處理tree
樹形,這種需求在工作中沒有遇到過,不知道以後會不會遇到。程式碼中的callee
是個指標,指向當前執行的函式,就是函式它自己,一旦是用於遞迴處理。如果不這樣寫的哈,就需要定義個函式變數,在函式內部呼叫自己。函式定義在reduce
的外面,程式碼的閱讀性降低。callee
就是提供快捷編碼,提高程式碼的閱讀性。
var data = [{
id: 1,
name: "辦公管理",
pid: 0,
children: [{
id: 2,
name: "請假申請",
pid: 1,
children: [
{ id: 4, name: "請假記錄", pid: 2 },
],
},
{ id: 3, name: "出差申請", pid: 1 },
]
}
];
const arr = data.reduce(function(pre,item){
const callee = arguments.callee //將執行函式賦值給一個變數備用
pre.push(item)
if(item.children && item.children.length > 0) item.children.reduce(callee,pre); //判斷當前引數中是否存在children,有則遞迴處理
return pre;
},[]).map((item) => {
//清空每一項item
item.children = []
return item
})
console.log(arr)
他還有個兄弟,叫reduceRight
,只是循序不一樣,一個是順序,一個是倒敘而已。功能幾乎一模一樣為什麼還要搞兩個?程式設計師有時真的是無聊到極點。我會不會去學reduceRight
?我不學,機智躲開。
有個大佬寫了reduce
的高階用法
https://developer.51cto.com/art/202002/610535.htm
三、some,every,filters,map (私人定製)
要想快速實現某些功能,就要使用功能專一的。
some
和 every
他們是兩兄弟,需要的引數可以是函式,返回值是true和false。前者有一個條件滿足,函式返回值是true
,就停止遍歷。後者是所有條件滿足,函式返回值是true
,就停止遍歷。
幾乎沒有用過,不知道使用場景。應該是算是否全勤,一個班的同學是否全部及格。還有全選效果的實現吧。
filter
(推薦)
官方的解釋是找到所有符合條件的元素然後放到一個陣列中 如果沒有符合條件的那麼返回空陣列。物如其名,就是一個過濾器,拿到符合條件的資料項拼成新的資料。
使用場景很多,渲染頁面的時候,需要拼接符合條件的陣列和給後臺資料的時候,給後臺符合條件的資料。按某個屬性分組。上一家公司的時候,寫跑步比賽頁面,頁面需要顯示跑步中,沒開始,跑完等狀態。此時如果使用filter
,就快速得到需要的陣列。
map
就是需要對一個陣列的每一項做相同的操作處理時。使用map
就會很快。map
不會改變原來的陣列這個定律適用於陣列成員是值型別。如果是引用型別,這個不適用。
那麼如何快速的選擇合適的遍歷陣列的方法呢?根據開發工作中的需求,首先審查功能專一私人定製的some,every,filters,map
,再到reduce
。這時候千萬不能輕易跳到for...of
。在reduce
這一層盡情耍大刀。如果實在不行了,就for...of
唄。
物件的遍歷
在工作中除了處理資料,處理物件也是常見。
一、for...in
這個是一般用法。使用很簡單。
二、keys,getOwnPropertyNames,ownKeys (推薦)
他們的使用一模一樣,都很簡單。 keys
遍歷自身的可列舉屬性。 getOwnPropertyNames
遍歷自身的所有屬性。 ownKeys
遍歷自身屬性(包括Symbol
)。
其實不亂的,需要知道屬性有可列舉與不可列舉,是否是Symbol。根據實質情況去選擇就行了。再說工作中大多數的物件的屬性都是可列舉的,用 keys
可以解決大多數需求。
為什麼要推薦使用 keys
等。可以試想一下,把物件轉化得到陣列之後,不就是可以使用上面陣列迴圈的那些方法處理各種邏輯?什麼reduce,filter,map,some,every
對吧,特別那個reduce
就和周杰倫雙截棍那樣,怎麼耍都可以。又快又簡潔。
var obj = {'0': 1, '1': b}
Object.keys(obj).forEach((key) =>{
console.log(key,obj[key])
})
在以前公司的專案中,我發現可以這樣寫介面,每次新增介面只需要在urlMap
物件裡面新增鍵值對:(或者說寫在其他js
檔案,把幾個物件合併到urlMap
)。
這樣處理的好處是,閱讀舒服,程式碼維護性高,減少不必要的程式碼衝突,節省時間。
const urlMap = {
loginUserInfo: '/api/loginUserInfo', //---方法名/介面路徑---
};
const services = {};
Object.keys(urlMap).forEach((methodName) = > {
services[methodName] = function (params, async = true, type = 'GET') {
var data = [];
var promise = $.ajax({
url: urlMap[methodName],
data: params ? params : {},
async: async,
dataType: "json"
});
if (async) {
return promise;
} else {
promise.done(({
code, obj
}) = > {
(code === 0) && (data = obj)
});
return data;
}
};
});
export default services;
說個題外話的,處理物件的時候,有時候需要做物件合併處理。物件合併可以用在哪些地方?就是頁面分頁元件,echart
元件它們需要配置物件,這個物件有很多的鍵值對。可以抽一些共同的屬性處理預設值寫在元件內部,其它的就寫在元件外部傳進來,這時候就需要合併它倆。這種概念叫做配置 config
。
簡單的寫法就是使用 Object.assingObj
;這個方法就是把多個源目標複製到目標物件,不管這個目標物件有沒有這個屬性值。
也可以使用嚴格一些的方法,就是說,目標物件和源物件公共的屬性值,才搞到目標物件上,不是的話不要過來,否則可能會搞壞內部的配置。缺點就是在元件寫配置物件時,需要多寫(有可能是很多行)屬性名,總是比每次寫在元件外面強(程式碼如下)。
function assignObj(vm, firstSource) {
for(let [index,item] of new Map([...arguments].map((item,i) => [i,item]))){
if(index === 0) continue; //躲開vm
let nextSource = [...arguments][index];
if (nextSource && typeof nextSource !== "object") continue;
Object.keys(vm).reduce((pre,cur) => {
if(vm.hasOwnProperty(cur) && nextSource.hasOwnProperty(cur))
vm[cur] = nextSource[cur]
},vm)
}
return vm
}
var returnValue = assignObj({name: 'name',age: 6,hairs: 8},{name: 'name',age: 6,clothes: 'lalala1'},{name: '周星馳'})
console.log(returnValue,'returnValue')
有人可能會提問?當物件裡面的屬性又是物件時,你這個方法不支援啊。難不倒我,我可以用callee
來升級的。
function assignObj(vm, firstSource) {
const callee = arguments.callee //將執行函式賦值給一個變數備用
for(let [index,item] of new Map([...arguments].map((item,i) => [i,item]))){
if(index === 0) continue; //躲開vm
let nextSource = [...arguments][index];
if (nextSource && typeof nextSource !== "object") continue;
Object.keys(vm).reduce((pre,cur) => {
if(Object.prototype.toString.call(vm[cur]) !== '[object Object]' && vm.hasOwnProperty(cur) && nextSource.hasOwnProperty(cur))
vm[cur] = nextSource[cur]
else if(Object.prototype.toString.call(vm[cur]) === '[object Object]' && vm.hasOwnProperty(cur) && nextSource.hasOwnProperty(cur))
callee(vm[cur],nextSource[cur]);
return vm;
},vm)
}
return vm
}
如果不想在vue
內部元件中寫太多的元件初始化配置值的話,可以採取一種寬鬆的方式:就是說在目標物件的基礎上,把所有源物件的屬性複製過來,共同屬性的值直接覆蓋。其實很簡單的,遍歷源目標,屬性是非物件把值複製過來,屬性是物件再次遍歷。 把上面程式碼的那行Object.keys
出現的vm
改為nextSource
就可以了。
再說個題外話吧,就是上個月啊,和一個後臺搞圖片的功能。圖片的待上傳列表是那後臺返回來的陣列。寫的時候,需要搞隱射,發現不好搞:一個頁面,拿到SPECIAL_FATE_STORE_HEADER
欄位給後臺specialFateStoreHeaderId
欄位;另外一個地方同理:specialFateStoreHeaderId->STORE_HEADER
。為什麼這麼麻煩?後臺小子邏輯差,經驗不足,沒處理好。一開始我使用switch case
。搞了不少行程式碼,維護性也不好,因為有兩套,改其中一個,另外一個也得跟著改。這時候,上面的那些陣列遍歷和物件遍歷的內容就可以用進來了。再一次證明會js
真的可以為所欲為,呵呵。程式碼如下。
let valueMap = {
SPECIAL_FATE_STORE_HEADER: 'specialFateStoreHeaderId'//值1:值2
//...這裡省略了15行
}
//獲取值的值
function getValueName(type) {
return valueMap[type] ? valueMap[type] : valueMap['SPECIAL_FATE_STORE_HEADER'];
}
//獲取鍵的值
function getKeyName(targetValue){
let targetArr = Object.keys(valueMap).filter((key) => { return valueMap[key] == targetValue });
return targetArr.length === 0 ? 'STORE_HEADER' : targetArr[0].split('FATE_')[1]
}
console.log(getValueName('SPECIAL_FATE_STORE_HEADER'),'valueMap')
console.log(getKeyName('specialFateStoreHeaderId'),'valueMap')
以後再次來需求,我就在valueMap
物件裡面加。萬一再來需求,後臺小子還要值3,值4怎麼辦?難不到我。我修改valueMap
的結構。再改下邏輯就行。他還要值5的話,那就叼人或者離職吧。
let valueMap = {
SPECIAL_FATE_STORE_HEADER: 'specialFateStoreHeaderId&&值3&&值4'//值1:值2 && 值3 && 值4
//...這裡省略了15行
}
最後,歡迎關注我的公眾號。