本文會先介紹
所有數(shù)組方法,再詳細介紹其中的reduce(引申閱讀:redux中的compose函數(shù)),接著介紹includes、indexOf、lastIndexOf與slice、splice參數(shù)為負值的時候會發(fā)生什么(引申閱讀:String中slice、substr、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]
說說結論:
- slice必須起點實際對應的角標比結束點實際的角標小才能截取出來東西
- slice和splice對于起點(終點)為負值就是從倒數(shù)的位置開始數(shù)
- 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)) //
說結論:
- slice和數(shù)組類似,必須頭小于尾才能截出東西
- substr第一個參數(shù)可以為負數(shù),代表倒數(shù),第二個參數(shù)不能為負數(shù),否則截不出東西
- 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 |
說說結論吧:
- for in是真的好用,數(shù)組對象通吃,但是速度是一如既往的倒數(shù)第一
- 真的不用太過于糾結for和forEach的性能區(qū)別,不緩存數(shù)組長度時性能基本相差無幾,for畢竟是最基礎的,但是forEach要比for好用一些,作者水平有效,扣不動js的底層實現(xiàn),但是循環(huán)最基本的應該還都是遍歷數(shù)組在堆中存儲的數(shù)據(jù)。
- 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
})
- 原生map性能飛升~我甚至懷疑是不是去年寫錯東西了,但是代碼是真的沒改過。。。
- for of作為es6提供,用來遍歷有Iterator接口數(shù)據(jù)類型的專用遍歷方法性能已經(jīng)逼近for和forEach
- 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}
完~ 感謝閱讀~ 一起進步!