在这里插入图片描述

前端对数组操作最为频繁,选择合适的数组循环遍历,可以提升开发效率。我们就通过这篇文章给大家总结一下数组常用的循环遍历方法,让大家一次性掌握这些方法的使用技巧。

在前端开发中,对数组的操作实在是太多了,比如后端为了减少服务器的压力,传输给前端的数据列表,并不和前端的需求相契合,那么前端就需要过滤整合。再比如前端采集了用户的选择,但是后端只需要传输指定字段,那么前端还需要对数据进行清洗后再提交。

这些操作会大量的对数组进行循环遍历,使用基础的for循环,基本上可以一招鲜吃遍天,但是你了解更多的循环遍历方法之后,可能会能更好的提升开发效率。

视频教程地址:https://www.bilibili.com/video/BV12c411k7sv/

一、 for循环和forEach对比

  • for循环是最常规最通用的一种循环遍历方法,后面要讲到的map、filter等都是可以通过for循环完成的;
  • forEach方法是一个高阶函数,会引入额外的函数调用开销;

注意:这种微小的性能差异通常在大多数应用中并不明显,除非在处理非常大的数据集时才可能显现出来。在实际开发中,应该优先考虑代码的可读性和维护性,而不是过度关注微小的性能差异。

1.性能上的比较

for > forEach

  • for循环直接操作索引,没有额外的函数调用和上下文,所以性能是最快的
  • for可以使用break终止,forEach不支持跳出循环

生成一个非常大的数组,循环遍历计算数组内的和,通过startTime和endTime计算出for循环的求和消耗时间

let arrs = [...Array(9999999).keys()]
let total = 0;
let startTime = Date.now();
for(let i=0; i<arrs.length; i++) total+=i;
let endTime = Date.now();
let countTime = endTime - startTime;
console.log("计数---->"+total);
console.log("消耗时间---->"+countTime);

//计数---->49999985000001
//消耗时间---->105

数组比较大,消耗的时间还是比较长的,假设循环遍历的时候,只要满足某个条件就跳出整个循环,那么就可以使用break进行跳出,这样能够节省很大的消耗,只需要再for循环内部加个条件即可,如下所示:

let arrs = [...Array(9999999).keys()]
let total = 0;
let startTime = Date.now();
for(let i=0; i<arrs.length; i++){
	if(i==10) break;
	total+=i;
}
let endTime = Date.now();
let countTime = endTime - startTime;
console.log("计数---->"+total);
console.log("消耗时间---->"+countTime);

计数---->45
消耗时间---->0

同样的代码,可以看出差异来,有break跳出循环太重要了,只要数组内满足了条件后续的循环遍历就是浪费,所以说可以通过for循环的break跳出循环,大大降低循环无用的消耗。

如果使用forEach循环,将无法使用break,不但不能跳出,只要写了break语法上就报错了;
但是可以在forEach内部可以使用return,满足条件使用return可以结束当前这次循环,但是剩下的循环还是继续跑完。

let arrs = [...Array(9999999).keys()]
let total = 0;
let startTime = Date.now();
arrs.forEach(item=>{
	return;
})
let endTime = Date.now();
let countTime = endTime - startTime;
console.log("计数---->"+total);
console.log("消耗时间---->"+countTime);

// 计数---->0
// 消耗时间---->91

在上面代码中,forEach内加不加return其实消耗的时间都是差不多相同的,return更像是for循环中的continue,无论如何都会遍历完全部之后才会执行forEach后面的代码。

但是在forEach中使用return可以节省计算时间,如下所示:

let arrs = [...Array(9999999).keys()]
let total = 0;
let startTime = Date.now();
arrs.forEach(item=>{
	total+=item;
})
let endTime = Date.now();
let countTime = endTime - startTime;
console.log("计数---->"+total);
console.log("消耗时间---->"+countTime);

// 计数---->49999985000001
// 消耗时间---->363

下面代码是在forEach内加上条件并return结束的代码:

let arrs = [...Array(9999999).keys()]
let total = 0;
let startTime = Date.now();
arrs.forEach(item=>{
    if(item>10)return;
	total+=item;
})
let endTime = Date.now();
let countTime = endTime - startTime;
console.log("计数---->"+total);
console.log("消耗时间---->"+countTime);

// 计数---->55
// 消耗时间---->103

如果if(item==10) return;类似于continue只会结束当次循环,后面的11、12…会继续++,所以消耗的计数会继续累加,循环次数继续全部完成,这样写不会节省时间;

如果if(item>10) return; 可以让10后面所有的计算全部退出,那么就不会再进行计数了,最终的结果肯定是10之前的计数,return后面所有的累计不计算了,所以节省了整个的计算时间。

2.异步同步化的支持度

在一些场景下,后端给前端提供的接口不提供批量查询,那么前端就需要将要查询的ID放置到数组中,通过循环遍历查询接口,前端最后组装并展现。

以jsonplaceholder接口为例,给大家分别用for循环和forEach循环来完成上面的操作,注意:这里的操作是为了做验证用的,如果真正要完成以上操作的话建议使用map+Promise.all来完成,在后面的map讲解中会给大家介绍到。

let arrs = Array.from({length:3},(_,index)=>index+1);
let datas=[];
arrs.forEach(async item=>{
	const {data} = await uni.request({
		url:"http://jsonplaceholder.typicode.com/posts/"+item
	})
	datas.push(data)
})
console.log(datas);

// []

在上面代码中uni.request返回promise对象,使用async/await是可以实现异步等待的,按照预想应该是forEach后面打印datas是一个数组对象,包含三次接口返回的数组,但是最好打印结果是空数组,说明在forEach函数内,不支持await异步等待。

可以将上面forEach代码改造成for循环:

async function fetchData() {
	let arrs = Array.from({length:3},(_,index)=>index+1);
	let datas=[];
	for (let i = 0; i < arrs.length; i++) {	
		const {data} = await uni.request({
			url:"http://jsonplaceholder.typicode.com/posts/"+arrs[i]
		});
		datas.push(data);
	}
	console.log(datas);
}
fetchData();

// [{...},{...},{...}]

改造成for循环之后,是可以按照预期拿到数组集合的,但是这种查询效率会非常低,我们这里是为了证明for循环是可以做异步等待的,要是真实的场景,建议看下面map+Promise.all的实现方法。

二、map()方法

map() 方法是数组原型的一个函数,对数组遍历不破坏原数组,将会创建一个新数组,按照原始数组元素顺序依次执行给定的函数,map方法非常适合用于处理数组中的每个元素并生成新的数组。

语法

  • map(callbackFn)
  • map(callbackFn, thisArg)

参数

callbackFn

  • element 数组中当前正在处理的元素。
  • index 正在处理的元素在数组中的索引。
  • array 调用了 map() 的数组本身。

thisArg

  • 执行 callbackFn 时用作 this 的值。

用法

1.将数组内每个元素×2后,获取新数组

let arrs = [1,2,3,4];
let newArrs = arrs.map(item=>item*2);
console.log(newArrs);

// [2, 4, 6, 8]

2.将数组对象内每个元素的名称拿出来,作为一个新数组

let arrs = [{name:"华为",price:6999},{name:"苹果",price:9888},{name:"小米",price:4999}]
let newArrs = arrs.map(item=>item.name);
console.log(newArrs);

// ['华为', '苹果', '小米']

3.将数组中原来的name不做任何改变,原有price:6999改为price:“6999元”,并且新增number属性值为888

let arrs = [{name:"华为",price:6999},{name:"苹果",price:9888},{name:"小米",price:4999}]
let newArrs = arrs.map(item=>{
	return {
		...item,
		price:item.price+"元",
		number:888
	}
});
console.log(newArrs);

//[{name: '华为', price: '6999元', number: 888},....]

返回的是每一个对象,要是想简化成一行,必须要将对象用括号包裹起来,如下所示:

let arrs = [{name:"华为",price:6999},{name:"苹果",price:9888},{name:"小米",price:4999}]
let newArrs = arrs.map(item=>({...item,price:item.price+"元",number:888}));
console.log(newArrs);

4.将原数组中属性进行更换,配合对象解构,可以更直观

let arrs = [{key:0,content:"篮球"},{key:1,content:"足球"},{key:2,content:"排球"}];
let newArrs = arrs.map(({key,content})=>({value:key,text:content}));
console.log(newArrs);

// [{value: 0, text: '篮球'},{value: 1, text: '足球'},{value: 2, text: '排球'}]

5.将异步请求map到新数组中,使用Promise.all同事处理多个Promise

下面这个代码块,是解决for循环异步同步化性能消耗过大的问题。

let arrs = Array.from({length:3},(_,index)=>index+1);
const promises = arrs.map(async (num) => {
    const result = await uni.request({
		url:"http://jsonplaceholder.typicode.com/posts/"+num
	})
    return result;
});
Promise.all(promises).then(res=>{
	console.log(res);
})

下面是Promise.all请求,network请求的状况,会同时发送请求,会节省很多的时间
在这里插入图片描述
下面是for循环异步同步化出现的网络请求状态,会等待一个完成之后再请求另一个,所以时间上会更慢一些
在这里插入图片描述

三、filter()方法

filter()过滤方法,会对原数组中的每个元素应用指定的函数,并返回一个新数组,其中包含符合条件的元素。原数组不会受到影响。

语法

  • map(callbackFn)
  • map(callbackFn, thisArg)

参数

callbackFn

  • element 数组中当前正在处理的元素。
  • index 正在处理的元素在数组中的索引。
  • array 调用了 map() 的数组本身。

thisArg

  • 执行 callbackFn 时用作 this 的值。

用法

在filter回调函数中,满足true即可被处理到新函数中,false不做处理。

1.找出数组中大于10的数据放到新数组中

let arrs = [5,7,8,15,22,1,2];
let newArrs = arrs.filter(item=>{
	return item>10
})
console.log(newArrs);

// [15,22]

如果是数组对象的话也是一样的,只需要找到满足的对象属性即可,比如找到数组对象中,年龄小于30岁的数据:

let arrs = [{name:"张三",age:16},{name:"李四",age:40},{name:"王五",age:28},{name:"汤姆",age:20}];
let newArrs = arrs.filter(item=>{
	return item.age<30
})
console.log(newArrs);

//[{name:"张三",age:16},...]

2.数组去重,结合indexOf方法

let arrs = [6,1,2,3,5,3,6];
let newArrs = arrs.filter((item,index,self)=>{
	return self.indexOf(item) === index
})
console.log(newArrs);

// [6, 1, 2, 3, 5]

3.数组对象中过滤掉无用数据

let arrs = [{id:1,name:"HTML5"},{id:2,name:"JavaScript"},{id:null,name:"小程序"},{name:"NodeJS"},{id:3,name:"VueJS"}];
let newArrs = arrs.filter(item=>{
	return item.id
})
console.log(newArrs);

// [ {id: 1, name: 'HTML5'},...]

4.filter和map组合使用,可以使用链式写法;首先去除无用数据,然后给每一条增加一个属性author

let arrs = [{id:1,name:"HTML5"},{id:2,name:"JavaScript"},{id:null,name:"小程序"},{name:"NodeJS"},{id:3,name:"VueJS"}];
let newArrs = arrs.filter(item=>{
	return item.id
}).map(item=>{
	return {
		...item,
		author:"咸虾米"
	}
})
console.log(newArrs);

// [{id: 1, name: 'HTML5', author: '咸虾米'},...]

四、reduce()方法

reduce() 方法对数组中的每个元素按序执行一个指定方法,每一次运行 reducer 会将先前元素的计算结果作为参数传入。

语法

  • reduce(callbackFn)
  • reduce(callbackFn, initialValue)

参数

callbackFn

  • prev(必填),上一次调用 callbackFn 的结果。
  • current(必填),当前元素的值。
  • index(可选),current 在数组中的索引位置。
  • array(可选),调用了reduce() 的数组本身。

initialValue

  • 第一次调用回调函数时初始值。如果指定初始值,则callbackFn从数组中的第一个值作为current开始执行;如果没有指定,数组中的第一个值是prev参数的值,数组第二个值是current参数的值;如果数组是空,则抛出错误,所以在不知道数组长度情况下可以指定初始值为0,保持逻辑的严谨性。

用法

1.数组求和 / 求积

let arrs = [1,2,3,4];
let result = arrs.reduce((prev,current,index)=>{	
	console.log(prev,current,index);
	return prev+current;
})
console.log("最终结果:"+result);

在这里插入图片描述
从上面结果可以看到,数组内有4位成员,但是函数执行了3次,这是因为没有初始值,所以prev是数组的第一位,current是数组的第二位,索引值也是从1开始的,假设指定了初始值,效果如下:

let arrs = [1,2,3,4];
let result = arrs.reduce((prev,current,index)=>{	
	console.log(prev,current,index);
	return prev+current;
},0)
console.log("最终结果:"+result);

在这里插入图片描述

2.求最大值/最小值

求数组中最大值:

let arrs = [5,6,1,22,3,7];
let result = arrs.reduce((prev,current,index)=>{	
	return Math.max(prev,current);
},0)
console.log(result);

// 22

求数组中最小值:

let arrs = [5,6,1,22,3,7];
let result = arrs.reduce((prev,current,index)=>{	
	return Math.min(prev,current);
},0)
console.log(result);

// 0

从以上代码可以看到,初始值也是在运算范围之内

3.对数组对象处理

数组对象求和,求数组对象中age的和:

let arrs = [{name:"张三",age:29},{name:"李四",age:16},{name:"王五",age:50},{name:"小明",age:21}];
let result = arrs.reduce((prev,current,index)=>{	
	return prev+current.age
},0)
console.log(result);

// 116

五、every()方法

判断数组中所有元素是否满足函数中给定的条件,全部满足返回true,只要有一项不满足则返回false。

语法

  • every(callbackFn)
  • every(callbackFn, thisArg)

参数

callbackFn

  • element 数组中当前正在处理的元素。
  • index 正在处理的元素在数组中的索引。
  • array 调用了 every() 的数组本身。

thisArg

  • 执行 callbackFn 时用作 this 的值。

用法

1.检查数组中的所有元素是否满足特定条件

let arrs = [1, 2, 3, 4, 5];
let result = arrs.every(num => num > 0);
console.log(result);

//true

2.检测数组对象中,是否所有商品都有库存

let arrs = [{name:"华为",price:5899,stock:true},{name:"苹果",price:9999,stock:false},{name:"小米",price:4399,stock:true},{name:"红米",price:899,stock:true}];
let result = arrs.every(item=>item.stock);
console.log(result);

// false

3.检测对象内所有属性是否都有值,配合Object.values()

如下图所示,表单中的提交按钮默认是禁用的,只有当表单中所有的输入框都输入值的情况下,按钮才会由禁用转为可用状态。
在这里插入图片描述

  • html布局部分
<view class="layout">
	<input type="text" v-model="obj.name" placeholder="请输入姓名">
	<input type="text" v-model="obj.age" placeholder="请输入年龄">
	<input type="text" v-model="obj.gender" placeholder="请输入性别">
	<input type="text" v-model="obj.like" placeholder="请输入爱好">
	<button type="primary" :disabled="state" @click="onSubmit">提交</button>
</view>
  • css样式
.layout{
	padding:30rpx;
	input{
		border:1px solid #e4e4e4;
		height: 80rpx;
		margin-bottom:20rpx;
		padding:0 20rpx;
	}
}
  • JS逻辑部分
    如果不使用every遍历的话,可能会使用如下方式:
<script setup>
import {computed, ref} from "vue";
const  obj = ref({
	name:"",
	age:"",
	gender:"",
	like:""
})
const state = computed(()=>{	
	if(obj.value.name && obj.value.age && obj.value.gender && obj.value.like){
		return false;
	}
	return true;	
})
function onSubmit(){
	console.log("提交表单");
}
</script>

简化计算属性也可以这样写:

const state = computed(() => !(obj.value.name && obj.value.age && obj.value.gender && obj.value.like));

如果使用Object.values()方法,可以将所有的值放入到一个数组中,然后对数组进行every循环遍历,最后得到条件的true或false,计算属性就可以这样做了:

const state = computed(()=>{	
   return !Object.values(obj.value).every(item=>item)
})

简化一下:

const state = computed(()=>!Object.values(obj.value).every(item=>item))

六、some()方法

some方法和every方法基本类似,只是some方法检测数组中,只要有一个满足条件即返回true,全部不满足条件才会返回false。

语法

  • some(callbackFn)
  • some(callbackFn, thisArg)

参数

callbackFn

  • element 数组中当前正在处理的元素。
  • index 正在处理的元素在数组中的索引。
  • array 调用了 some() 的数组本身。

thisArg

  • 执行 callbackFn 时用作 this 的值。

用法

1.检查数组中的是否有满足特定条件的元素

let arrs = [55,26,3,12,39];
let result  = arrs.some(item=>item<10);
console.log(result);

// true

2.检查数组对象中,是否有满足price小于1000的元素,并且有stock库存

let arrs = [{name:"华为",price:5899,stock:true},{name:"苹果",price:9999,stock:false},{name:"小米",price:4399,stock:true},{name:"红米",price:899,stock:true}];
let result = arrs.some(item=>item.price<1000 && item.stock);
console.log(result);

// true

3.将every表单示例改造成some方法

1)使用some完成全部对象有值的判断
const state = computed(()=>Object.values(obj.value).some(item=>!item))
2)只要有一个表单有值就将禁用改可用
const state = computed(()=>!Object.values(obj.value).some(item=>item))

七、includes()方法

includes() 方法用来判断一个数组是否包含一个指定的值,根据情况,如果包含则返回 true,否则返回 false。
注意:includes() 方法只能用于检测基础数据类型(如数字、字符串、布尔值),而不能用于检测包含对象或其他数组的二维数组。如果需要检测对象或其他复杂数据类型,可以使用其他方法或自定义函数来实现。

语法

  • includes(searchElement)
  • includes(searchElement, fromIndex)

参数

searchElement

  • 需要查找的值。

fromIndex

用法

1.基础用法

const arrs = [1, 2, 3];
console.log(arrs.includes(2));

// true

2.every和includes配合,检测一个数组是否包含另一个数组

let arrs1 = [1,2,3,4,5,6,7];
let arrs2 = [2,5,7];
let arrs3 = [1,6,9];
let result1 = arrs2.every(item=>arrs1.includes(item));
let result2 = arrs3.every(item=>arrs1.includes(item));
console.log(result1);
console.log(result2);

// true
// false

七、find()和findIndex()-findLast()和findLastIndex()

待更新…

结语

大家也可以结合视频教程对照我写的文档进行学习。
视频教程地址:https://www.bilibili.com/video/BV12c411k7sv/

该课程主要讲解我在开发中常用的循环遍历方法,当然会结合大量的案例展现不同的用法,视频及文档中的知识点依赖于mdn文档和我的案例整理。

如果还有没讲到的,希望大家留言,我会继续进行补充的。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐