JS数组方法的总结与浅析

本文会先介绍所有数组方法,再详细介绍其中的reduce(引申阅读:redux中的compose函数),接着介绍includesindexOflastIndexOfslicesplice参数为负值的时候会发生什么(引申阅读:String中slicesubstrsubstring方法有什么区别),最后对数组常用遍历方法的性能做简要分析(引申阅读:数组中valueskeysentries方法的基础使用方法)。

数组方法整合

随便定义一个数组,然后查看他的原型链,你会发现:
(在使用方法中: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]

说说结论:
1. slice必须起点实际对应的角标比结束点实际的角标小才能截取出来东西
2. slice和splice对于起点(终点)为负值就是从倒数的位置开始数
3. 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)) // 

说结论:
1. slice和数组类似,必须头小于尾才能截出东西
2. substr第一个参数可以为负数,代表倒数,第二个参数不能为负数,否则截不出东西
3. 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

说说结论吧:
1. for in是真的好用,数组对象通吃,但是速度是一如既往的倒数第一
2. 真的不用太过于纠结for和forEach的性能区别,不缓存数组长度时性能基本相差无几,for毕竟是最基础的,但是forEach要比for好用一些,作者水平有效,扣不动js的底层实现,但是循环最基本的应该还都是遍历数组在堆中存储的数据。
3. 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
})
  1. 原生map性能飞升~我甚至怀疑是不是去年写错东西了,但是代码是真的没改过。。。
  2. for of作为es6提供,用来遍历有Iterator接口数据类型的专用遍历方法性能已经逼近for和forEach
  3. 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}

完~ 感谢阅读~ 一起进步!

发表评论