JS數(shù)組方法的總結與淺析

本文會先介紹所有數(shù)組方法,再詳細介紹其中的reduce(引申閱讀:redux中的compose函數(shù)),接著介紹includes、indexOf、lastIndexOfslice、splice參數(shù)為負值的時候會發(fā)生什么(引申閱讀:String中slicesubstr、substring方法有什么區(qū)別),最后對數(shù)組常用遍歷方法的性能做簡要分析(引申閱讀:數(shù)組中values、keys、entries方法的基礎使用方法)。

數(shù)組方法整合

隨便定義一個數(shù)組,然后查看他的原型鏈,你會發(fā)現(xiàn):
(在使用方法中:a代表調(diào)用方法的數(shù)組,[...]/[,...]/[,...[,...]]代表可選參數(shù))

數(shù)組的原型上大致有這些方法 數(shù)組的構造函數(shù)上大致有這些方法 作用 返回值 是否改變原數(shù)組 使用方法
1. concat ? 數(shù)組拼接 新的數(shù)組 a.concat(b)
2. copyWithin ? 值覆蓋 數(shù)組自身 a.copyWithin(target[, start[, end]])
3. entries ? 遍歷 由[key,value]組成的迭代器 a.entries()
4. every ? 條件判斷器 布爾值(是否所有成員都滿足條件) a.every(callback[, thisArg])
5. fill ? 固定值填充 數(shù)組自身 a.fill(value[, start[, end]])
6. filter ? 條件過濾器 新的數(shù)組 a.filter(callback(element[, index[, array]])[, thisArg])
7. find ? 條件選擇器 undefined或第一個滿足條件的數(shù)組成員 a.find(callback[, thisArg])
8. findIndex ? 條件索引查詢 -1或第一個滿足條件的數(shù)組角標 a.findIndex(callback[, thisArg])
9. flat ? 數(shù)組扁平化 新數(shù)組 a.flat(depth)
10. flatMap ? map+深度為1的flat 新數(shù)組 a.flatMap(callback(currentValue[, index[, array]])[, thisArg])
11. forEach ? 遍歷器 a.forEach(callback(currentValue[, index[, array]])[, thisArg])
? from 類數(shù)組或帶有interator接口的對象轉(zhuǎn)換為數(shù)組 轉(zhuǎn)化后的數(shù)組 --- Array.from(類數(shù)組/帶有interator接口的對象)
12. includes ? 判斷器 布爾值 a.includes(valueToFind[, fromIndex(可為負)])
13. indexOf ? 索引查詢 -1或第一個匹配的數(shù)組角標 a.indexOf(searchElement[, fromIndex(可為負)])
? isArray 判斷是否是數(shù)組 布爾值 --- Array.isArray(待檢測對象)
14. join ? 字符串拼接合成 字符串 a.join([separator(默認為 ",")])
15. keys ? 遍歷 由key組成的迭代器 a.keys()
16. lastIndexOf ? 反向索引查詢 -1或第一個匹配的數(shù)組角標 a.lastIndexOf(searchElement[, fromIndex = a.length - 1(可為負值)])
17. map ? 遍歷器 新的數(shù)組 a.map(function callback(currentValue[, index[, array]])[, thisArg])
? of 統(tǒng)一規(guī)則的創(chuàng)建數(shù)組 創(chuàng)建出來的新數(shù)組 --- Array.of() => []; Array.of(3) => [3]; Array.of(item1,...,itemN)
18. pop ? 刪除尾部成員 刪除的成員 a.pop()
19. push ? 添加新的尾部成員 原數(shù)組增加成員后的長度 a.push(item1, ..., itemN)
20. reduce ? 正序迭代器 成員迭代完成后的值 a.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])
21. reduceRight ? 倒序迭代器 成員迭代完成后的值 a.reduceRight(callback(accumulator, currentValue[, index[, array]])[, initialValue])
22. reverse ? 數(shù)組翻轉(zhuǎn) 數(shù)組自身 a.reverse()
23. shift ? 頭部刪除 刪除的成員 a.shift()
24. slice ? 數(shù)組截取 截取出來的成員 a.slice([begin[, end]]);
25. some ? 條件判斷器 布爾值(是否有滿足條件的數(shù)組成員) a.some(callback(element[, index[, array]])[, thisArg])
26. sort ? 排序 數(shù)組自身 a.sort([compareFunction(默認為元素按照轉(zhuǎn)換為的字符串的各個字符的Unicode位點進行排序)])
27. splice ? 刪除(并插入新的成員) 刪除的成員 a.splice(start[, deleteCount[, item1[, item2[, ...]]]])
28. toLocaleString ? 帶配置的字符串轉(zhuǎn)化 字符串 a.toLocaleString([locales[,options]]);
29.toString ? 字符串轉(zhuǎn)化 字符串 a.toString()
30. unshift ? 頭部插入新的成員 原數(shù)組增加成員后的長度 a.unshift(item1, ..., itemN)
31. values ? 遍歷 由value組成的迭代器 a.values()

reduce方法講解

可能大家都聽說過或者試過reduce函數(shù),但是在小需求背景下較難有機會使用,reduce函數(shù)接收2個參數(shù):

Array.prototype.reduce = function (fn, initValue) {
// 其中fn包含4個參數(shù),初始值,當前數(shù)組成員,當前數(shù)組角標,數(shù)組本身
...
}
Array.reduce(function(Accumulator, CurrentValue, CurrentIndex, SourceArray ) {...}, initValue)

簡易的使用方法:

// 初始值為1,迭代次數(shù)為4 => 1+1、2+2、4+3、7+4
[1,2,3,4].reduce((a, b, c, d) => a + b,1) // 輸出1+1+2+3+4 => 11
// 初始值缺省,數(shù)組的第一個值會被當做初始值,迭代次數(shù)為3 => 1+2、3+3、6+4
[1,2,3,4].reduce((a, b, c, d) => a + b) // 輸出1+2+3+4 => 10

// 通過reduce獲取最大值
let arr = [2,100,38,250,3,9]
arr.reduce((a, b) => a > b ? a : b)
// 當然我們還可以這樣獲取最大值
Math.max.apply(null, arr)
Math.max.call(null, ...arr)
Math.max(...arr)
// 嗯。。?;蛘呤謱懸粋€Max函數(shù)

較為貼切實際的應用:假如我們有一個需求,要對一個數(shù)組進行多種處理,比如翻轉(zhuǎn),濾除所有不是Number的成員,濾除所有大于10的Number,然后返回一個用逗號分隔的字符串。

    let arr = ['c', 'x', 'k', 123, 456, 10, 8, 100, 4, 3, 125, 3]

    function reverse (arr) {
        return arr.reverse()
    }

    function deleteUnNumber (arr) {
        return arr.filter(function(item) {
            return Object.prototype.toString.call(item) === '[object Number]';
        });
    }

    function deleteOverTen (arr) {
        return arr.filter(function(num) {
            return num < 10;
        });
    }

    function join (arr) {
        return arr.join(',')
    }
    
    // 不使用reduce
    join(deleteOverTen(deleteUnNumber(reverse(arr)))) // 3,3,4,8

    // 使用reduce,數(shù)組內(nèi)的順序就是函數(shù)的執(zhí)行順序
    [reverse, deleteUnNumber, deleteOverTen, join].reduce((a, b) => b(a), arr) // 3,3,4,8

可能有人會問為啥不這么寫

    arr.reverse().filter(function(item) {
        return Object.prototype.toString.call(item) === '[object Number]' && item < 10;
    }).join(',')

這個例子比較簡單,往往實際上reduce的擴展僅對接收參數(shù)的類型判斷可能就不止這些代碼,自己語義化封裝獨立的函數(shù)將顯得十分必要,而且不可能所有的代碼都會是原型鏈上現(xiàn)成的函數(shù),函數(shù)的返回值也不會如此單調(diào),日常生產(chǎn)中,(據(jù)我所知)與reduce聯(lián)系最為緊密的應該是redux中的compose函數(shù),用于擴展增強store,源碼如下:

export default function compose(...funcs) {
    if (funcs.length === 0) {
        return arg => arg
    }

    if (funcs.length === 1) {
        return funcs[0]
    }

    return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

compose函數(shù)的作用是將傳入的函數(shù)進行包裝,靠前的函數(shù)后執(zhí)行,大致行為如下:
compose(a,b,c) (...args)=> a(b(c(..args)))
利用compose改寫上面的例子

    let arr = ['c', 'x', 'k', 123, 456, 10, 8, 100, 4, 3, 125, 3]
    
    function reverse (arr) {
        return arr.reverse()
    }

    function deleteUnNumber (arr) {
        return arr.filter(function(item) {
            return Object.prototype.toString.call(item) === '[object Number]';
        });
    }

    function deleteOverTen (arr) {
        return arr.filter(function(num) {
            return num < 10;
        });
    }

    function join (arr) {
        return arr.join(',')
    }

    compose(join,deleteOverTen,deleteUnNumber,reverse)(arr) // 3,3,4,8
    // 等同于 join(deleteOverTen(deleteUnNumber(reverse(arr)))) // 3,3,4,8,二者函數(shù)的順序從直觀上保持一致了

includes、indexOf、lastIndexOf 索引為負值的時候

Mozilla上對第二個參數(shù)的描述是這樣的!明明基本都是一個東西,愣是說出3套

includes indexOf lastIndexOf
開始查找的位置。如果該索引值大于或等于數(shù)組長度,意味著不會在數(shù)組里查找,返回-1。如果參數(shù)中提供的索引值是一個負值,則將其作為數(shù)組末尾的一個抵消,即-1表示從最后一個元素開始查找,-2表示從倒數(shù)第二個元素開始查找 ,以此類推。 注意:如果參數(shù)中提供的索引值是一個負值,并不改變其查找順序,查找順序仍然是從前向后查詢數(shù)組。如果抵消后的索引值仍小于0,則整個數(shù)組都將會被查詢。其默認值為0. 從fromIndex 索引處開始查找 valueToFind。如果為負值,則按升序從 array.length + fromIndex 的索引開始搜 (即使從末尾開始往前跳 fromIndex 的絕對值個索引,然后往后搜尋)。默認為 0。 從此位置開始逆向查找。默認為數(shù)組的長度減 1,即整個數(shù)組都被查找。如果該值大于或等于數(shù)組的長度,則整個數(shù)組會被查找。如果為負值,將其視為從數(shù)組末尾向前的偏移。即使該值為負,數(shù)組仍然會被從后向前查找。如果該值為負時,其絕對值大于數(shù)組長度,則方法返回 -1,即數(shù)組不會被查找。

1.大白話總結一下就是-1就是從倒數(shù)第一個開始找
2.當負數(shù)大到超過數(shù)組length的時候,正向查詢那倆不受影響,等于還是從頭查找,lastIndexOf由于是往前查找,所以就沒數(shù)據(jù)可查,返回-1

    var a = [1,2,3,2,2]

    console.log(a.includes(2)) // true 
    console.log(a.indexOf(2)) // 1
    console.log(a.lastIndexOf(2)) // 4

    // 默認狀態(tài)
    console.log(a.includes(2, 0)) //從角標0開始往后找 true 
    console.log(a.indexOf(2, 0)) //從角標0開始往后找 1
    console.log(a.lastIndexOf(2, 4)) //從(正數(shù)第四個)角標4開始往前找 4
    // 等同于
    console.log(a.lastIndexOf(2, -1)) // 從(倒數(shù)第一個)角標4開始往前找 4

    console.log(a.includes(2, 0)) // true 
    console.log(a.indexOf(2, 0)) // 1
    console.log(a.lastIndexOf(2, 0)) // -1

    console.log(a.includes(2, -1)) // true 
    console.log(a.indexOf(2, -1)) // 4
    console.log(a.lastIndexOf(2, -1)) // 4

    console.log(a.includes(2, -2)) // true 
    console.log(a.indexOf(2, -2)) // 3
    console.log(a.lastIndexOf(2, -2)) // 3

    console.log(a.includes(2, -20)) // true 
    console.log(a.indexOf(2, -20)) // 1
    console.log(a.lastIndexOf(2, -20)) // -1

slice和splice參數(shù)為負值的時候

    var arr = [1,2,3,4,5,6,7]

    console.log(arr.slice(2,3))   // [3]
    console.log(arr.slice(3,2))   // []
    console.log(arr.slice(-3,-2)) // [5]
    console.log(arr.slice(-2,-3)) // []
    console.log(arr.slice(2,-3))   // [3,4]
    console.log(arr.slice(-7,7))   // [1, 2, 3, 4, 5, 6, 7]

    // 雖然寫成這樣,假設splice之間不相互影響
    console.log(arr.splice(2,3))   // [3,4,5]
    console.log(arr.splice(3,2))   // [4, 5]
    console.log(arr.splice(-3,-2)) // []
    console.log(arr.splice(-2,-3)) // []
    console.log(arr.splice(2,-3))  // []
    console.log(arr.splice(-3,1))  // [5]
    console.log(arr.splice(-7,7))  // [1, 2, 3, 4, 5, 6, 7]

說說結論:

  1. slice必須起點實際對應的角標比結束點實際的角標小才能截取出來東西
  2. slice和splice對于起點(終點)為負值就是從倒數(shù)的位置開始數(shù)
  3. splice刪除個數(shù)為負則不刪除

引申到字符串slice、substr、substring三個截取函數(shù)的區(qū)別:

    var a = '123456789'

    console.log(a.slice())     // 123456789
    console.log(a.substr())    // 123456789
    console.log(a.substring()) // 123456789

    console.log(a.slice(2))     // 3456789
    console.log(a.substr(2))    // 3456789
    console.log(a.substring(2)) // 3456789

    console.log(a.slice(2,5))     // 345
    console.log(a.substr(2,5))    // 34567
    console.log(a.substring(2,5)) // 345

    console.log(a.slice(6,1))     // 
    console.log(a.substr(6,1))    // 7
    console.log(a.substring(6,1)) // 23456

    console.log(a.slice(2,-1))     // 345678
    console.log(a.substr(2,-1))    // 
    console.log(a.substring(2,-1)) // 12

    console.log(a.slice(6,-3))     // 
    console.log(a.substr(6,-3))    // 
    console.log(a.substring(6,-3)) // 123456

    console.log(a.slice(-4,2))     // 
    console.log(a.substr(-4,2))    // 67
    console.log(a.substring(-4,2)) // 12

    console.log(a.slice(-4,-3))     // 6
    console.log(a.substr(-4,-3))    // 
    console.log(a.substring(-4,-3)) // 

說結論:

  1. slice和數(shù)組類似,必須頭小于尾才能截出東西
  2. substr第一個參數(shù)可以為負數(shù),代表倒數(shù),第二個參數(shù)不能為負數(shù),否則截不出東西
  3. substring會把負數(shù)自動歸0,然后從兩個參數(shù)較小的那個截取到較大的那個,所以同時為負,截不出東西

數(shù)組遍歷和性能問題

網(wǎng)上百度出來的15~17年底的老文章基本都是說for的性能是幾倍于foreach/明顯快于foreach的,但是你寫的js代碼,瀏覽器肯定是需要解析的。就和上古時代你說字符串拼接的性能極差,內(nèi)存占用高,需要定義一個數(shù)組,然后array.push()然后再array.join('')這樣,才能優(yōu)化性能,可是人家瀏覽器早就優(yōu)化過了,字符串拼接的性能已經(jīng)高于數(shù)組操作了,循環(huán)又何嘗不是,我們做一個測試。造一個全是1的長度為1000w的數(shù)組,然后遍歷這個數(shù)組并干點啥,比如把這些1拿出來賦值,或者原地++,我們看看性能對比,map只遍歷不return新數(shù)組。
PS: 代碼效率和你電腦的硬件配置,跑代碼時電腦的狀態(tài)都會有關,我家的臺式機跑同樣的代碼比這個筆記本要快上很多。
先說說18年9月份當時的數(shù)據(jù),當時的絕版數(shù)據(jù)大致是這樣的(單位毫秒),為了確保一定的公平性,foreach的回調(diào)里用的array和index進行的數(shù)組操作,沒有直接用item:

for for in 利用array和index操作的forEach for of 原生map jq.each jq.map filter
111 5121 98 407 1776 165 10 153
114 5066 98 582 1816 180 10 149

2019年5月15日,電腦同一個(一個超飽和的。。。15款8g的macbook pro),效果如下

for 緩存數(shù)組length的for for in 回調(diào)函數(shù)傳3個參數(shù)并利用array和index操作的forEach 回調(diào)函數(shù)傳3個參數(shù)并利用item操作的forEach 回調(diào)函數(shù)傳2個(不傳array)參數(shù)并利用item操作的forEach for of 原生map jq.each jq.map filter
117 33 4592 136 104 164 161 177 192 18 193
187 32 4768 140 110 159 179 182 207 22 187
121 33 4552 118 102 150 161 183 190 18 183

說說結論吧:

  1. for in是真的好用,數(shù)組對象通吃,但是速度是一如既往的倒數(shù)第一
  2. 真的不用太過于糾結for和forEach的性能區(qū)別,不緩存數(shù)組長度時性能基本相差無幾,for畢竟是最基礎的,但是forEach要比for好用一些,作者水平有效,扣不動js的底層實現(xiàn),但是循環(huán)最基本的應該還都是遍歷數(shù)組在堆中存儲的數(shù)據(jù)。
  3. forEach只傳2個參數(shù)(item, index)要比傳3個參數(shù)(item, index, array)的運行速度,即使我根本沒有使用第三個參數(shù),代碼如下:
let temp = null
let array = new Array(10000000).fill(1)

array.forEach((item, index, arrays) => { // 1000w次循環(huán) 耗時100ms左右
    temp = ++item
})

array.forEach((item, index) => {// 1000w次循環(huán) 耗時150ms左右
    temp = ++item
})
  1. 原生map性能飛升~我甚至懷疑是不是去年寫錯東西了,但是代碼是真的沒改過。。。
  2. for of作為es6提供,用來遍歷有Iterator接口數(shù)據(jù)類型的專用遍歷方法性能已經(jīng)逼近for和forEach
  3. jquery的map雖然是穩(wěn)定最快的,為啥?憑啥?但是jq已經(jīng)淡出歷史的舞臺了,既往不咎~附上jq的map源碼(看不懂有啥玄機,有興趣你們可以分析分析)。
// 這...憑啥能更快?
map: function( elems, callback, arg ) {
        var length, value,
            i = 0,
            ret = [];

        // Go through the array, translating each of the items to their new values
        if ( isArrayLike( elems ) ) {
            length = elems.length;
            for ( ; i < length; i++ ) {
                value = callback( elems[ i ], i, arg );

                if ( value != null ) {
                    ret.push( value );
                }
            }

        // Go through every key on the object,
        } else {
            for ( i in elems ) {
                value = callback( elems[ i ], i, arg );

                if ( value != null ) {
                    ret.push( value );
                }
            }
        }

        // Flatten any nested arrays
        return concat.apply( [], ret );
    },

瀏覽器內(nèi)核在升級,解析方式也在改變,比如前幾天Google IO 2019表示JS解析又快了兩倍,async執(zhí)行快了。。。嗯11倍!所以loop性能的變動也合情合理吧?

說到遍歷不得不在最后談一下values、keys、entries

    var arr = [1, 3, 'a', 'asd', {a: 123}]
    var entries = arr.entries()
    var keys = arr.keys()
    var values = arr.values()

    // entries、keys、values打印出來都是Array Iterator {}  不能直接通過角標訪問數(shù)據(jù),提供以下幾種遍歷方法
    // for of遍歷
    for (item of entries) {
        console.log(item) // [0, 1]  [1, 3]  [2, "a"]  [3, "asd"]  [4, {a: 123}]
    }
    // 轉(zhuǎn)化為數(shù)組遍歷
    var keys_arr = [...keys] // [0, 1, 2, 3, 4]

    //通過next()遍歷
    console.log(values.next().value) // 1
    console.log(values.next())       // {value: 3, done: false}
    console.log(values.next().value) // a
    console.log(values.next().value) // asd
    console.log(values.next().value) // {a: 123}
    console.log(values.next().value) // undefined
    console.log(values.next())       // {value: undefined, done: true}

完~ 感謝閱讀~ 一起進步!

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容