你的数组缺爱吗?请容我帮你找回

1,236 阅读12分钟

在JavaScript的数组方法中,有些方法不知道你曾经是否吐槽过

  • 为什么只有正向查找的 findfindIndex,而没有反向查找lastFindlastFindIndex
  • 为什么没有查询满足条件的所有索引方法 findIndexs
  • 为什么我想删除数组里某些索引对应的值,没有正确删除delByIndexs方法
  • 为什么没有加减乘除哪些方法
  • ...
  • 为什么

是的,它本身是没有提供,但是已有的方法是可以帮助我们实现这些功能,好吧,我们下面用行动来找回数组缺失的爱吧,用以增强我们项目中的掌控数组的灵活度,下面开始我们的正题。

反向查询索引 - lastFindIndex

我们知道findIndex: 从索引0开始正向开始查询,但是没有反向的

const list = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }]
list.findIndex(i => i.id == 3)//2
list.findIndex(i => i.id == 33)//-1

对着findIndex 来实现我们的 lastFindIndex,为防JavaScript不知什么时候就添加这个方法,万一名字还TM一样,所有不建议在原型上直接扩展,下面我们自己实现

/*
const lastFindIndex = function (arr, cb) {
    // 克隆倒序
    const list = arr.slice().reverse()
    // 利用已有的正向查找方法 findIndex 去查询
    const index = list.findIndex(cb)
    // 找不到就是 -1 ,找的到就正常计算,list.length - 1 是数组的最后一个索引值
    return index === -1 ? -1: list.length - 1 - index
}
*/
const lastFindIndex = function (arr, cb) {
    for (let i = arr.length - 1; i >= 0; i--) {
        if (cb(arr[i])) return i
    }
    return -1
}
const list = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }, { id: 3 }, { id: 7 }]
lastFindIndex(list, i => i.id == 3) // 5
lastFindIndex(list, i => i.id == 33) // -1

反向查找值 - lastFind

find 也是从索引0 开始从索引0开始正向开始查询,找到第一个就返回,找不到就undefined,但是却没有反向查询方法

const list = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }, { id: 3 }, { id: 7 }]
list.find(i => i.id == 3) //{id: 3}
list.find(i => i.id == 33) // undefined

对着find 来实现我们的 lastFind,下面我们自己实现

/*
const lastFind = function (arr, cb) {
    return arr.slice().reverse().find(cb)
}
*/
const lastFind = function (arr, cb) {
    for (let i = arr.length - 1; i >= 0; i--) {
        if (cb(arr[i])) return arr[i]
    }
}
const list = [
    { type: 1, subType: 11 },
    { type: 2, subType: 22 },
    { type: 3, subType: 33 },
    { type: 4, subType: 44 },
    { type: 5, subType: 55 },
    { type: 3, subType: 34 },
    { type: 7, subType: 77 }
]
list.find(i => i.type == 3) //{type: 3, subType: 33}
lastFind(list, i => i.type == 3) //{type: 3, subType: 34}
lastFind(list, i => i.type == 33) //undefined

如果你想找到所有满足条件的就用filter吧

查询满足条件的所有索引 - findIndexs

  • 有时候,你想获取数组中满足条件的值所有的对应的索引值,由于 findIndex 只能返回一个,所以啊你就想...,要是它出现就好了
const findIndexs = function (arr, cb) {
    const ret = []
    for (let i = 0; i < arr.length; i++) {
        if (cb(arr[i])) {
            ret.push(i)
        }
    }
    return ret
}
const list = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }, { id: 6 }, { id: 7 }]
findIndexs(list, i => [2, 4, 7].includes(i.id)) // [1, 3, 6]

删除数组多项值 - delByIndexs

  • 有些时候,你想,获取到满足条件的所有索引值后, 你想要去删除对应索引的值,但是正方向删除又会改变数组的长度,导致后面删除的值没有对应上,所以啊你又想...然后出现了

没有对应上示例如下:

const delIndexs = [1, 3, 6]
const list = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }, { id: 6 }, { id: 7 }]
delIndexs.forEach(i => {
    list.splice(i, 1)
})
// i=1时,删除正确的此时数组=> [{ id: 1 }, { id: 3 }, { id: 4 }, { id: 5 }, { id: 6 }, { id: 7 }]
// i=3时,删除正确的此时数组=> [{ id: 1 }, { id: 3 }, { id: 4 }, { id: 6 }, { id: 7 }]
// i=6时,删除正确的此时数组=> [{ id: 1 }, { id: 3 }, { id: 4 }, { id: 6 }, { id: 7 }]
// 因为数组长度已改变,找不到要删除的索引 6

// 最终数组变成=> [{ id: 1 }, { id: 3 }, { id: 4 }, { id: 6 }, { id: 7 }]

// 但是我们的预期是:[{ id: 1 }, { id: 3 },  { id: 5 }, { id: 6 }]

delByIndexs 实现思路如下,从后往前删除总得没错吧,emm...

const delByIndexs = function (arr, delIndexs,isDeep = true) {
    // 是否克隆,有时候你不想影响原数组,就需要克隆
    if(isDeep) arr = JSON.Parse(JSON.stringify(arr))
    // 先排序成降序,从后往前删除
    delIndexs = delIndexs.sort((a, b) => b - a)
    for (let i = 0; i < delIndexs.length; i++) {
        arr.splice(delIndexs[i], 1)
    }
    return arr
}

const list = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }, { id: 6 }, { id: 7 }]

// findIndexs 在上面已经实现,查询满足条件的所有所有值
const delIndexs = findIndexs(list, i => [2, 4, 7].includes(i.id)) // [1, 3, 6]

delByIndexs(list, delIndexs) // [{ id: 1 }, { id: 3 },  { id: 5 }, { id: 6 }]

数组重复 - arrayRepeat

字符串都有 repeat 方法,数组咋就没有呢,没有关系,我们自己造

/**
 * @description: 数组复制
 * @param {Array} arr:需要复制的数组
 * @param {Number} n: 复制的次数,默认 0
 */
const arrayRepeat = function(arr, n = 0) {
    let base = 0
    let res = arr
    while (base < n) {
        res = res.concat(arr)
        base++
    }
    return res
}
arrayRepeat([1, 2, 3]) // [1, 2, 3]    不传复制次数 默认是不复制 返回原数组
arrayRepeat([1, 2, 3], 1) //  [1, 2, 3, 1, 2, 3] 复制 1 遍
arrayRepeat([1, 2, 3], 2) // [1, 2, 3, 1, 2, 3, 1, 2, 3] 复制 2 遍
arrayRepeat([1, 2, 3], 3) // [1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3] 复制 3 遍
arrayRepeat([{ name: 'lisi' }], 1) // [{name: "lisi"},{name: "lisi"}] 复制 1 遍

数组截取 - arraySubstr

人家字符串都有 substr,为什么数组就不能拥有一个呢,你说气不气,不过,没有关系,用自己灵活的双手,造一个就是啦

/**
 * @description: 让数组拥有和字符串的substr 一样的功能
 * @param { Array } arr:数组
 * @param { Number } startIndex:开始截取的索引值
 * @param { Number} len:截取的长度
 */
function arraySubstr(arr, startIndex, len) {
    return arr.slice(startIndex, startIndex + len)
}
let arr = [1, 2, 3, 4, 5]

arraySubstr(arr, 0, 1) // [1]
arraySubstr(arr, 0, 2) // [1,2]
arraySubstr(arr, 0, 3) // [1,2,3]
arraySubstr(arr, 1, 3) // [2,3,4]

'12345'.substr(0,1) // '1'
'12345'.substr(0,2) // '12'
'12345'.substr(0,3) // '123'
'12345'.substr(1,3) // '234'

数组值访问 - arrayAt

不知道你听说 Array的at方法没,不过谷歌浏览器已经实现了,兼容还不是很好,如果你想马上立刻就要用,不过,没有关系,你可以自己写一个,先了解下这个 Array.prototype.at,它解决了什么痛点:但有时我们希望从末尾访问元素,而不是从开始访问元素。例如,访问数组的最后一个元素:

const arr = [1,2,3,4,5]
// 数组最后一项
const lastItem = arr[ arr.length - 1]

arr[arr.length - 1]是访问数组最后一个元素的方式,其中arr.length - 1是最后一个元素的索引。 问题在于方括号访问器不允许直接从数组末尾访问项,也不接受负下标。

因此,一个新的提议(截至2021年1月的第3阶段)将at()方法引入了数组(以及类型化的数组和字符串),并解决了方括号访问器的诸多限制。

what?21年提议? 咱们自己实现一个也简单,怕什么,不要怂!

/**
 * @description: 根据索引访问数组项
 * @param { Array } arr:数组
 * @param { Number } index:索引,默认 0
 */
const arrayAt = function (arr, index = 0) {
    // 数组长度
    const len = arr.length
    return index < 0 ? arr[len + index] : arr[index]
}
// 谷歌浏览器测试,如果你的谷歌浏览器不行,可能就是版本不是最新的
[1,2,3,4].at() // 1
[1,2,3,4].at(1) // 2
[1,2,3,4].at(2) // 3
[1,2,3,4].at(3) // 4
[1,2,3,4].at(4) // undefined

[1,2,3,4].at(-1) // 4
[1,2,3,4].at(-2) // 3
[1,2,3,4].at(-3) // 2
[1,2,3,4].at(-4) // 1
[1,2,3,4].at(-5) // undefined

arrayAt([1,2,3,4]) // 1
arrayAt([1,2,3,4],1) // 2
arrayAt([1,2,3,4],2) // 3
arrayAt([1,2,3,4],3) // 4
arrayAt([1,2,3,4],4) // undefined

arrayAt([1,2,3,4],-1) // 4
arrayAt([1,2,3,4],-2) // 3
arrayAt([1,2,3,4],-3) // 2
arrayAt([1,2,3,4],-4) // 1
arrayAt([1,2,3,4],-5) // undefined

stringAt 我们一笔带过

/**
 * @description: 根据索引访问字符串某个索引值
 * @param { String } str
 * @param { Number } index:索引,默认 0
 */
const stringAt = function (str, index = 0) {
    // 字符串长度
    const len = str.length
    return index < 0 ? str[len + index] : str[index]
}

基础分组 - group

数组没有分组方法,只有可以用来实现分组相关slice方法

/**
 * @description: 一维数组转二维数组 (分组)
 * @param {Array} arr:数组
 * @param {Number} num: 平分基数(num 个为一组进行分组
 */
const group = function (arr, num) {
    return [...Array(Math.ceil(arr.length / num)).keys()].reduce((p, _, i) => [...p, arr.slice(i * num, (i + 1) * num)], [])
}
group([1,2,3,4,5,6,7,8,9,10],2) // [[1,2],[3,4],[5,6],[7,8],[9.10]]
group([1,2,3,4,5,6,7,8,9,10],3) // [[1,2,3],[4,5,6],[7,8,9],[10]]

实现思路解析:

// 通过 [...Array( num ).keys()] 方式快速创建升序数组
[...Array(2).keys()] // [0, 1]
[...Array(3).keys()] // [0, 1, 2]

// 通过 Math.ceil(arr.length / num) 向下取整,计算出可以分成多少个组
const arr = [1,2,3,4,5,6,7,8,9,10]
const num = 3 //平分基数(每份多个个)
Math.ceil(arr.length / num) // 4

// group([1,2,3,4,5,6,7,8,9,10],3)=>[[1,2,3],[4,5,6],[7,8,9],[10]]
// 我们需求利用数组切割方法 slice 来进行切割,slice 切割 包含 头不包含尾
// [1,2,3,4,5,6,7,8,9,10].slice(0,3) // [1, 2, 3]
// [1,2,3,4,5,6,7,8,9,10].slice(3,6) // [4, 5, 6]
// [1,2,3,4,5,6,7,8,9,10].slice(6,9) // [7, 8, 9]
// [1,2,3,4,5,6,7,8,9,10].slice(9,12)// [10]

// 然后需要 借助 数组的 reduce 方法,首先 reduce 的第一个参数是函数,函数参数第三项正好是索引
// reduce 的第二个参数 可选,可以自定义第一项 p 的初始值,我们传入一个空数组 []
// i - 索引 num - 平分基数
i*num (0*3)~ (i+1)*num ((0+1)*3)=> 0 ~ 3
i*num (1*3)~ (i+1)*num ((1+1)*3)=> 3 ~ 6
i*num (2*3)~ (i+1)*num ((2+1)*3)=> 6 ~ 9
i*num (3*3)~ (i+1)*num ((3+1)*3)=> 9 ~ 12
// 至此,我们找到了切割的索引计算规律
// 最后就是处理结果的随着遍历的不停的合并结果,直到遍历完成,返回处理后的结果即可

MDN -JavaScript reduce

条件分组 - groupArchive

针对json数组的一个条件归档

/**
 * @description:归档, 对一维 json 数组进行归档(根据 key)
 * @param {Array} arr:一维数组
 * @param {String} key:key 字符串
 */
const groupArchive = function (arr, key) {
  return [...new Set(arr.map(i => i[key]))].reduce((p, c) => [...p, arr.filter(i => i[key] === c)], [])
}
let books = [
    { date: '2月', name: '化学书' },
    { date: '1月', name: '历史书' },
    { date: '2月', name: '数学书' },
    { date: '3月', name: '语文书' },
    { date: '1月', name: '地理书' }
]
groupArchive(books, 'date')
/*
[
    [
        {date: "2月", name: "化学书"}
        {date: "2月", name: "数学书"}
    ],
     [
        {date: "1月", name: "历史书"}
        {date: "1月", name: "地理书"}
    ],
     [
        {date: "3月", name: "语文书"}
    ],
]        
*/

状态分组 - groupState

  • josn 数组按条件分组,顺序不变

示例:

 const list = [ 

    {name:'1',type:0}, 
    {name:'2',type:1}, 
    {name:'3',type:1}, 
    {name:'4',type:1}, 

    {name:'5',type:0}, 
    {name:'6',type:0}, 

    {name:'7',type:2}, 
    {name:'8',type:2}, 
    {name:'9',type:2}, 

    {name:'10',type:0},
    {name:'11',type:0}, 

  ]
/*
  [ 
    [{name:'1',type:0}], 
    
    [{name:'2',type:1}, {name:'3',type:1}, {name:'4',type:1}], 
    
    [{name:'5',type:0}, {name:'6',type:0}], 
    
    [{name:'7',type:2}, {name:'8',type:2}, {name:'9',type:2}], 
    
    [{name:'10',type:0},{name:'11',type:0}], 
  ]
 */
/**
 * @description: 数组按标识进行分组 (分组后顺序不变)
 * @param {Array} list:分组的数组
 * @param {String} typeStr:分组的标识
 * @return {Array}
 */

const groupState = function(list,typeStr){
    const ret = [] 
    let p = 0
    let n = 0
    for(let i=1,len=list.length;i<len;i++){
      const pre = list[i-1]
      const cur = list[i]
      if(pre[typeStr]!==cur[typeStr]){
        n = i
        ret.push(list.slice(p,n))
        p = i
      }
      if(i===len-1)ret.push(list.slice(p))
    }
    return ret
}


/**
 * 示例:
 * 
 * const list = [ 

    {name:'1',type:0}, 
    {name:'2',type:1}, 
    {name:'3',type:1}, 
    {name:'4',type:1}, 

    {name:'5',type:0}, 
    {name:'6',type:0}, 

    {name:'7',type:2}, 
    {name:'8',type:2}, 
    {name:'9',type:2}, 

    {name:'10',type:0},
    {name:'11',type:0}, 

  ]
 * 需求=> 转换成
 *  [ 
    [{name:'1',type:0}], 
    
    [{name:'2',type:1}, {name:'3',type:1}, {name:'4',type:1}], 
    
    [{name:'5',type:0}, {name:'6',type:0}], 
    
    [{name:'7',type:2}, {name:'8',type:2}, {name:'9',type:2}], 
    
    [{name:'10',type:0},{name:'11',type:0}], 
  ]
 * 
 * groupState(list,'type')
 */

升序数组生成 - rangeGenerater

有时候,你想快速生成一个升序数组,来实现其它业务逻辑, 这个时候,你有个更棒的ieda,不需要去for循环,这里有个更好的 rangeGenerater

/**
 * @description: 生成 起止数字间(包含起止数字)的升序数组
 * @param {Number} min : 最小值
 * @param {Number} max :最大值
 */
const rangeGenerater = function (min, max) {
    return Array.from({ length: max - min + 1 }, (_, i) => i + min)
}
rangeGenerater(5,10) // [5, 6, 7, 8, 9, 10]
rangeGenerater(0,10)//[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

尽管 [...Array( num ).keys()] 也可以快速生成0 - num 的升序数组,但是不够灵活,不能设置区间范围,当然你也可以借助 它 做其它事。

四舍五入 - round

有时候,计算价格,总价什么的,需要用到对总价进行四舍五入

/**
 * 四舍五入到指定位数
 * @param {Number} n:小数
 * @param {Number} decimals :四舍五入到指定位数
 */
const round = function (n, decimals) {
    if (decimals === undefined) return n
    return Number(Math.round(n + 'e' + (decimals || 0)) + 'e-' + (decimals || 0))
}
round(0) // 0
round(1.23456,1)// 1.2
round(1.23456,2)// 1.23
round(1.23456,3)// 1.235
round(1.23456,4)// 1.2346
round(1.23456,5)// 1.23456

1.0450.toFixed(2) // "1.04"
round(1.0450,2) // 1.05

计算 - calc

数字计算无非就是加减乘除

// 本方法 依赖上面的 四舍五入round 方法
/**
 * 计算方法 calc
 * @param { number } type :0 加  1 减 2 乘 3 除
 * @param { String | Number } a :计算数a
 * @param { String | Number } b :计算数b
 * @param { Number } digit  :结果保留的位数
 * @return Number | String
 */
const calc = function (type, a, b, digit) {
    let r1, r2
    try {
        r1 = a.toString().split('.')[1].length
    } catch (e) {
        r1 = 0
    }
    try {
        r2 = b.toString().split('.')[1].length
    } catch (e) {
        r2 = 0
    }
    let maxLen = Math.pow(10, Math.max(r1, r2))
    let tyeps = [
        round((Math.round(maxLen * a) + Math.round(maxLen * b)) / maxLen, digit), //加
        round((Math.round(maxLen * a) - Math.round(maxLen * b)) / maxLen, digit), //减
        round((Math.round(maxLen * a) * Math.round(maxLen * b)) / (maxLen * maxLen), digit), //乘
        round(Math.round(maxLen * a) / Math.round(maxLen * b), digit) //除
    ]
    let str = String(round(tyeps[type], digit || 0))
    if (digit) {
        if (str.includes('.')) return str.split('.')[0] + '.' + str.split('.')[1].padEnd(digit, 0)
        return (str + '.').padEnd((str + '.').length + digit, 0)
    } else {
        return tyeps[type]
    }
}
 
// 减法 -
calc(0,2,2,2) //'0.00'
// 加法 +
calc(1,2,2,2) //'4.00'
// 乘法 ×
calc(2,2,3,2) //'6.00'
// 除法 ÷
calc(3,2,3,2) //'0.67'

这个计算方法其实做个计算的辅助方法还是可以的,真的加减乘除还是有点鸡肋

加法 - add

只是两个数两家求和? 不不不,灵活性不高,我们要既可以两数相加,也可以多数累加求和,对的,你并不贪心,需求如此,本函数 依赖上面的 计算方法 calc

// 本函数 依赖上面的 计算方法 calc
/**
 * 相加
 * @param {Number} a :加数
 * @param {Number} b :被加数
 * @param {Number} digit :结果保留位数
 */
const add = function (a, b, digit) {
    return Array.isArray(a) ? (a.length ? a.reduce((p, c) => add(p, c, b), 0) : 0) : calc(0, a, b, digit)
}
/**
 *
 * 示例:
 * 0.1 + 0.2 的和 四舍五入到第三位小数
 * add(0.1,0.2,3) //"0.300"
 *
 */
/**
 *
 * 示例:多数相加,第一个参数为累加数数组,第二个参数为和的四舍五入到的小数位数
 *
 * add([0.1,0.2]) // 0.3
 * add([0.1,0.2],3)     // '0.300'
 * add([0.1,0.2,1,2],3) // '3.300'
 *
 */

减法 - subtract

功能和加法一样,本函数 依赖上面的 计算方法 calc

// 本函数 依赖上面的 计算方法 calc
/**
 * 两数相减
 * @param {Number} a :减数
 * @param {Number} b :被减数
 * @param {Number} digit :结果保留位数
 */
const subtract = function (a = 0, b = 0, digit) {
    return Array.isArray(a) ? (a.length ? a.reduce((p, c) => subtract(p, c, b)) : 0) : calc(1, a, b, digit)
}
/**
 * 示例:
 *
 * subtract(0.1,0.12)   // -0.02
 * subtract(0.1,0.12,0) // 0
 * subtract(0.1,0.12,1) // "0.0"
 * subtract(0.1,0.12,2) // "0.02"
 * subtract(0.1,0.12,3) // "-0.020"
 *
 */
/**
 * 示例:
 *
 * subtract([1.1,3]) // -1.9
 * subtract([1.1,3],1) // "-1.9"
 * subtract([1.1,3],2) // "-1.90"
 *
 */

乘法 - multiply

本函数 依赖上面的 计算方法 calc,多数相乘,两数相乘,保留位数

/**
 * 两数相乘
 * @param {*} a :乘数
 * @param {*} b :被乘数
 * @param {*} digit :结果保留位数
 */
const multiply = function (a, b, digit) {
    return Array.isArray(a) ? (a.length ? a.reduce((p, c) => multiply(p, c, b), 1) : 0) : calc(2, a, b, digit)
}
/**
 * 示例:
 *
 * multiply(1.1,2.2)    // 2.42
 * multiply(1.13,0.8,0) // 1
 * multiply(1.13,0.8,1) // "0.9"
 * multiply(1.13,0.8,2) // "0.90"
 * multiply(1.13,0.8,3) // "0.904"
 * multiply(1.13,0.8,4) // "0.9040"
 *
 */

除法 - devide

本函数 依赖上面的 计算方法 calc,多数相除,两数相除,保留位数

/**
 * 两数相除
 * @param {Number} a :除数
 * @param {Number} b :被除数
 * @param {Number} digit :结果保留位数
 */
const devide = function (a, b, digit) {
    return Array.isArray(a) ? (a.length >= 2 ? a.reduce((p, c) => devide(p, c, b)) : '') : !a || !b ? '' : calc(3, a, b, digit)
}
/**
 * 示例:
 *
 * devide() // ""
 * devide(1) // ""
 * devide(1,3) // 0.3333333333333333
 * devide(1,3,1) // "0.3"
 * devide(1,3,2) // "0.33"
 *
 */
/***
 * 示例:
 *
 * devide() // ""
 * devide([]) // ""
 * devide([1,3]) // 0.3333333333333333
 * devide([1,3,3]) // 0.1111111111111111
 * devide([1,9]) // 0.1111111111111111
 * devide([1,9],0) // 0
 * devide([1,9],1) // "0.1"
 * devide([1,9],2) // "0.11"
 * devide([1,9],3) // "0.111"
 *
 */

后言

推荐3个在线代码生成和运行工具

代码在线生成:carbon

1111.gif

  1. 生成好看的代码片段

在线前端开发工具:codepen

GIF 2021-9-4 13-53-04.gif

  1. 平时练手
  2. 发文章时写小 demo
  3. 提问题时,附上重现的 codepen

JavaScript线上沙箱环境 codesandbox

GIF 2021-9-4 13-56-30.gif


周末时间就为完成这篇文章,希望对大家有所帮助,集思广益是我最初的初衷,同时提高自己的写作水平,让文章更浅显易懂,如果你觉得本篇文章对你有哪怕那么一丁点有用,请勿吝啬你的👍,我会继续努力!

如果文章里有所错误,请留言指出,如果你有更好的idea,欢迎留言,集思广益,共同进步!

最后我们一起跟着运动运动,干我们这行的,长时间坐在电脑面前,容易得颈椎病! d239e2d9-1515-4180-aff8-f2576f8bdf01.gif