在JavaScript的数组方法中,有些方法不知道你曾经是否吐槽过
- 为什么只有正向查找的
find、findIndex,而没有反向查找的lastFind、lastFindIndex - 为什么没有查询满足条件的所有索引方法
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
// 至此,我们找到了切割的索引计算规律
// 最后就是处理结果的随着遍历的不停的合并结果,直到遍历完成,返回处理后的结果即可
条件分组 - 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
- 生成好看的代码片段
在线前端开发工具:codepen
- 平时练手
- 发文章时写小 demo
- 提问题时,附上重现的 codepen
JavaScript线上沙箱环境 codesandbox
周末时间就为完成这篇文章,希望对大家有所帮助,集思广益是我最初的初衷,同时提高自己的写作水平,让文章更浅显易懂,如果你觉得本篇文章对你有哪怕那么一丁点有用,请勿吝啬你的👍,我会继续努力!
如果文章里有所错误,请留言指出,如果你有更好的idea,欢迎留言,集思广益,共同进步!
最后我们一起跟着运动运动,干我们这行的,长时间坐在电脑面前,容易得颈椎病!