本文会先介绍
所有数组方法
,再详细介绍其中的reduce
(引申阅读:redux
中的compose
函数),接着介绍includes
、indexOf
、lastIndexOf
与slice
、splice
参数为负值的时候会发生什么(引申阅读:String中slice
、substr
、substring
方法有什么区别),最后对数组常用遍历方法的性能做简要分析(引申阅读:数组中values
、keys
、entries
方法的基础使用方法)。
数组方法整合
随便定义一个数组,然后查看他的原型链,你会发现:
(在使用方法中:a代表调用方法的数组,[...]/[,...]/[,...[,...]]代表可选参数)
数组的原型上大致有这些方法 | 数组的构造函数上大致有这些方法 | 作用 | 返回值 | 是否改变原数组 | 使用方法 |
---|---|---|---|---|---|
1. concat | ✅ | 数组拼接 | 新的数组 | 否 | a.concat(b) |
2. copyWithin | ✅ | 值覆盖 | 数组自身 | 是 | a.copyWithin(target[, start[, end]]) |
3. entries | ✅ | 遍历 | 由[key,value]组成的迭代器 | 否 | a.entries() |
4. every | ✅ | 条件判断器 | 布尔值(是否所有成员都满足条件) | 否 | a.every(callback[, thisArg]) |
5. fill | ✅ | 固定值填充 | 数组自身 | 是 | a.fill(value[, start[, end]]) |
6. filter | ✅ | 条件过滤器 | 新的数组 | 否 | a.filter(callback(element[, index[, array]])[, thisArg]) |
7. find | ✅ | 条件选择器 | undefined或第一个满足条件的数组成员 | 否 | a.find(callback[, thisArg]) |
8. findIndex | ✅ | 条件索引查询 | -1或第一个满足条件的数组角标 | 否 | a.findIndex(callback[, thisArg]) |
9. flat | ❌ | 数组扁平化 | 新数组 | 否 | a.flat(depth) |
10. flatMap | ❌ | map+深度为1的flat | 新数组 | 否 | a.flatMap(callback(currentValue[, index[, array]])[, thisArg]) |
11. forEach | ✅ | 遍历器 | 无 | 否 | a.forEach(callback(currentValue[, index[, array]])[, thisArg]) |
❌ | from | 类数组或带有interator接口的对象转换为数组 | 转化后的数组 | --- | Array.from(类数组/带有interator接口的对象) |
12. includes | ✅ | 判断器 | 布尔值 | 否 | a.includes(valueToFind[, fromIndex(可为负)]) |
13. indexOf | ✅ | 索引查询 | -1或第一个匹配的数组角标 | 否 | a.indexOf(searchElement[, fromIndex(可为负)]) |
❌ | isArray | 判断是否是数组 | 布尔值 | --- | Array.isArray(待检测对象) |
14. join | ✅ | 字符串拼接合成 | 字符串 | 否 | a.join([separator(默认为 ",")]) |
15. keys | ✅ | 遍历 | 由key组成的迭代器 | 否 | a.keys() |
16. lastIndexOf | ✅ | 反向索引查询 | -1或第一个匹配的数组角标 | 否 | a.lastIndexOf(searchElement[, fromIndex = a.length - 1(可为负值)]) |
17. map | ✅ | 遍历器 | 新的数组 | 否 | a.map(function callback(currentValue[, index[, array]])[, thisArg]) |
❌ | of | 统一规则的创建数组 | 创建出来的新数组 | --- | Array.of() => []; Array.of(3) => [3]; Array.of(item1,...,itemN) |
18. pop | ✅ | 删除尾部成员 | 删除的成员 | 是 | a.pop() |
19. push | ✅ | 添加新的尾部成员 | 原数组增加成员后的长度 | 是 | 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 | ✅ | 数组翻转 | 数组自身 | 是 | a.reverse() |
23. shift | ✅ | 头部删除 | 删除的成员 | 是 | a.shift() |
24. slice | ✅ | 数组截取 | 截取出来的成员 | 否 | a.slice([begin[, end]]); |
25. some | ✅ | 条件判断器 | 布尔值(是否有满足条件的数组成员) | 否 | a.some(callback(element[, index[, array]])[, thisArg]) |
26. sort | ✅ | 排序 | 数组自身 | 是 | a.sort([compareFunction(默认为元素按照转换为的字符串的各个字符的Unicode位点进行排序)]) |
27. splice | ✅ | 删除(并插入新的成员) | 删除的成员 | 是 | a.splice(start[, deleteCount[, item1[, item2[, ...]]]]) |
28. toLocaleString | ❌ | 带配置的字符串转化 | 字符串 | 否 | a.toLocaleString([locales[,options]]); |
29.toString | ❌ | 字符串转化 | 字符串 | 否 | a.toString() |
30. unshift | ✅ | 头部插入新的成员 | 原数组增加成员后的长度 | 是 | a.unshift(item1, ..., itemN) |
31. values | ✅ | 遍历 | 由value组成的迭代器 | 否 | a.values() |
reduce方法讲解
可能大家都听说过或者试过reduce函数,但是在小需求背景下较难有机会使用,reduce函数接收2个参数:
Array.prototype.reduce = function (fn, initValue) {
// 其中fn包含4个参数,初始值,当前数组成员,当前数组角标,数组本身
...
}
Array.reduce(function(Accumulator, CurrentValue, CurrentIndex, SourceArray ) {...}, initValue)
简易的使用方法:
// 初始值为1,迭代次数为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
// 初始值缺省,数组的第一个值会被当做初始值,迭代次数为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函数
较为贴切实际的应用:假如我们有一个需求,要对一个数组进行多种处理,比如翻转,滤除所有不是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,数组内的顺序就是函数的执行顺序
[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的扩展仅对接收参数的类型判断可能就不止这些代码,自己语义化封装独立的函数将显得十分必要,而且不可能所有的代码都会是原型链上现成的函数,函数的返回值也不会如此单调,日常生产中,(据我所知)与reduce联系最为紧密的应该是redux中的compose函数,用于扩展增强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函数的作用是将传入的函数进行包装,靠前的函数后执行,大致行为如下:
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,二者函数的顺序从直观上保持一致了
includes、indexOf、lastIndexOf 索引为负值的时候
Mozilla上对第二个参数的描述是这样的!明明基本都是一个东西,愣是说出3套
includes | indexOf | lastIndexOf |
---|---|---|
开始查找的位置。如果该索引值大于或等于数组长度,意味着不会在数组里查找,返回-1。如果参数中提供的索引值是一个负值,则将其作为数组末尾的一个抵消,即-1表示从最后一个元素开始查找,-2表示从倒数第二个元素开始查找 ,以此类推。 注意:如果参数中提供的索引值是一个负值,并不改变其查找顺序,查找顺序仍然是从前向后查询数组。如果抵消后的索引值仍小于0,则整个数组都将会被查询。其默认值为0. | 从fromIndex 索引处开始查找 valueToFind。如果为负值,则按升序从 array.length + fromIndex 的索引开始搜 (即使从末尾开始往前跳 fromIndex 的绝对值个索引,然后往后搜寻)。默认为 0。 | 从此位置开始逆向查找。默认为数组的长度减 1,即整个数组都被查找。如果该值大于或等于数组的长度,则整个数组会被查找。如果为负值,将其视为从数组末尾向前的偏移。即使该值为负,数组仍然会被从后向前查找。如果该值为负时,其绝对值大于数组长度,则方法返回 -1,即数组不会被查找。 |
1.大白话总结一下就是-1就是从倒数第一个开始找
2.当负数大到超过数组length的时候,正向查询那俩不受影响,等于还是从头查找,lastIndexOf由于是往前查找,所以就没数据可查,返回-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
// 默认状态
console.log(a.includes(2, 0)) //从角标0开始往后找 true
console.log(a.indexOf(2, 0)) //从角标0开始往后找 1
console.log(a.lastIndexOf(2, 4)) //从(正数第四个)角标4开始往前找 4
// 等同于
console.log(a.lastIndexOf(2, -1)) // 从(倒数第一个)角标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参数为负值的时候
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对于起点(终点)为负值就是从倒数的位置开始数
- splice删除个数为负则不删除
引申到字符串slice、substr、substring三个截取函数的区别:
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和数组类似,必须头小于尾才能截出东西
- substr第一个参数可以为负数,代表倒数,第二个参数不能为负数,否则截不出东西
- substring会把负数自动归0,然后从两个参数较小的那个截取到较大的那个,所以同时为负,截不出东西
数组遍历和性能问题
网上百度出来的15~17年底的老文章基本都是说for的性能是几倍于foreach/明显快于foreach的,但是你写的js代码,浏览器肯定是需要解析的。就和上古时代你说字符串拼接的性能极差,内存占用高,需要定义一个数组,然后array.push()然后再array.join('')这样,才能优化性能,可是人家浏览器早就优化过了,字符串拼接的性能已经高于数组操作了,循环又何尝不是,我们做一个测试。造一个全是1的长度为1000w的数组,然后遍历这个数组并干点啥,比如把这些1拿出来赋值,或者原地++,我们看看性能对比,map只遍历不return新数组。
PS: 代码效率和你电脑的硬件配置,跑代码时电脑的状态都会有关,我家的台式机跑同样的代码比这个笔记本要快上很多。
先说说18年9月份当时的数据,当时的绝版数据大致是这样的(单位毫秒),为了确保一定的公平性,foreach的回调里用的array和index进行的数组操作,没有直接用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 | 缓存数组length的for | for in | 回调函数传3个参数并利用array和index操作的forEach | 回调函数传3个参数并利用item操作的forEach | 回调函数传2个(不传array)参数并利用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是真的好用,数组对象通吃,但是速度是一如既往的倒数第一
- 真的不用太过于纠结for和forEach的性能区别,不缓存数组长度时性能基本相差无几,for毕竟是最基础的,但是forEach要比for好用一些,作者水平有效,扣不动js的底层实现,但是循环最基本的应该还都是遍历数组在堆中存储的数据。
- forEach只传2个参数(item, index)要比传3个参数(item, index, array)的运行速度慢,即使我根本没有使用第三个参数,代码如下:
let temp = null
let array = new Array(10000000).fill(1)
array.forEach((item, index, arrays) => { // 1000w次循环 耗时100ms左右
temp = ++item
})
array.forEach((item, index) => {// 1000w次循环 耗时150ms左右
temp = ++item
})
- 原生map性能飞升~我甚至怀疑是不是去年写错东西了,但是代码是真的没改过。。。
- for of作为es6提供,用来遍历有Iterator接口数据类型的专用遍历方法性能已经逼近for和forEach
- jquery的map虽然是稳定最快的,为啥?凭啥?但是jq已经淡出历史的舞台了,既往不咎~附上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 );
},
浏览器内核在升级,解析方式也在改变,比如前几天Google IO 2019表示JS解析又快了两倍,async执行快了。。。嗯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 {} 不能直接通过角标访问数据,提供以下几种遍历方法
// for of遍历
for (item of entries) {
console.log(item) // [0, 1] [1, 3] [2, "a"] [3, "asd"] [4, {a: 123}]
}
// 转化为数组遍历
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}
完~ 感谢阅读~ 一起进步!