js使用經驗--遍歷

stephenWu5發表於2020-04-19

目的

在平常的前端開發中,一般需要處理資料(陣列和物件居多),特別是複雜功能的頁面,通常是一到兩個物件陣列(有時陣列裡面還有陣列)。大多數前端開發的難點就是這裡,耗時大。以前我在工作中,遇到的支付方式功能,排課日曆,場地預約,公園大螢幕運動排行和彈幕,後臺系統的許可權模組等等,這些功能難度大費腦耗時間多。其他那些功能很簡單的,無腦複製貼上執行就完成了。如果我可以總結一下,找出一些高效的處理陣列和物件的方法,以後工作中就對號入座的使用,這肯定可以提高我的工作效率,到時候我可以多出時間來給測試或者學習新知識或者摸魚休息。這總比每天瞎用js工具類,碌碌無為的寫程式碼強。

做一件事有很多種方法,最快捷的就那一兩個,把它們找出來,事半功倍。正所謂磨刀不誤砍柴工。

空虛

陣列的遍歷

一、for...of(通用)

最好用的迭代(迴圈)語句

說到迴圈,javascript的迴圈語句那麼多,我該如何選擇?其實就是問下自己,自己需要什麼?我眼睛一閉,我要可以用在很多地方(物件),寫法簡單,這樣維護性高;我還要支援continue,break,throw,return打斷迴圈或者跳到下一個迴圈(某一次迭代不執行迴圈體程式碼塊),我還要indexindex等於某個值時做一些邏輯判斷。

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 (私人定製)

要想快速實現某些功能,就要使用功能專一的。

someevery

他們是兩兄弟,需要的引數可以是函式,返回值是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行
}

javascript.jpg

最後,歡迎關注我的公眾號。

公眾號二維碼.jpg

相關文章