最近看了好几篇关于forEach
的文章,问如何跳出forEach
循环,还有问forEach
和for
循环有什么区别,看着我是一脸无奈,他们是猩猩吗?
如何中断forEach
先来说说跳出forEach
,你都要跳出forEach
,那么可不可以不要用forEach
呀?可不可以先去看看文档再来去使用它呀?MDN->forEach赶紧先去看看文档。
先来说一下文档中没有一句提到通过抛出异常来中断forEach
循环的,而是告诉你如果你要中断forEach
循环就不要使用forEach
,来我们看看有哪些方式可以代替forEach
:
const arr = [];
// 原型方法
arr.filter(() => {
});
arr.map(() => {
});
arr.find(() => {
});
arr.findIndex(() => {
});
arr.lastIndexOf(() => {
});
arr.reduce(() => {
});
arr.reduceRight(() => {
});
arr.some(() => {
});
arr.every(() => {
});
// js语法
for (let i = 0; i < arr.length; i++) {
}
for (let arrElement of arr) {
}
for (let index in arr) {
}
来看看,有这么多的方式可以替代forEach
,为什么非要抓着forEach
不放呢?
看评论区有说应付面试的,你面试题就不能问问数组的原型方法有哪些可以迭代数组的?就不能问问for of
和for in
的区别?
不吐槽了,上面有这么多方案都可以达到和forEach
相同的效果,那么可以中断迭代的方案有哪些呢?
在讲解方案之前,我先带大家简单的认识一下forEach
,forEach
是Array
的原型方法,首先它是内置的方法,它接收一个回调函数,这个回调函数接收三个参数,第一个是当前迭代的对象,第二个是当前迭代的索引,第三个是原数组,同时他还有第二个参数,就是回调函数的this
,百闻不如一见:
/**
* forEach 的函数签名
* @param {function} callback 回调函数
* callback 的函数签名
* @param {any} currentValue 当前元素
* @param {number} index 当前元素的索引
* @param {array} array 当前数组
*
* @param {any} thisArg 回调函数中的 this 指向
*/
// forEach(callbackfn: (value: T, index: number, array: T[]) => void, thisArg?: any): void;
// forEach 的用法
arr.forEach(function (item, index, thisArr) {
console.log(item, index, thisArr);
console.log(this);
}, {
a: 'a'});
认识完了之后,接下来就是用上面的方案替换forEach
循环
- 第一波推荐js基础,for 循环
```js
function forEachCallback(item, index, thisArr) {
console.log(item, index, thisArr);
console.log(this);
}
// forEach 的用法
arr.forEach(forEachCallback, {a: 'a'});
// 转换为 for 循环
for (let i = 0; i < arr.length; i++) {
forEachCallback.call({a: 'a'}, arr[i], i, arr);
}
for (let arrElement of arr) {
forEachCallback.call({a: 'a'}, arrElement, arr.indexOf(arrElement), arr);
}
for (let index in arr) {
forEachCallback.call({a: 'a'}, arr[index], index, arr);
}
上面的代码可以说是手写了一半的`forEach`了,现在的问题是怎么中断这个循环,可以看到业务逻辑还是放到方法里面了,方法里面写`return`没用,写`break`报错(这些都是很基础的知识点了),这样也就能理解为什么`forEach`不能被中断了。
现在都写了`for`循环了,只需要把业务代码直接移到循环体里面,然后对应的写`break`不就可以中断了。
> 下面就是误人子弟的代码,后面都用这个例子来转换
```js
try {
arr.forEach((item) => {
// 我们假设有5%的几率会出现异常,然后中断循环
if (Math.random() < 0.05) {
throw new Error('中断循环');
}
console.log(item);
});
} catch (e) {
}
转换为for
循环
for (let i = 0; i < arr.length; i++) {
if (Math.random() < 0.05) {
break;
}
console.log(arr[i]);
}
for (let arrElement of arr) {
if (Math.random() < 0.05) {
break;
}
console.log(arrElement);
}
for (const index in arr) {
if (Math.random() < 0.05) {
break;
}
console.log(arr[index]);
}
some
和every
来认识一下这两个
Array
的原型方法:some
:测试是否有一个元素通过了回调函数的测试。/** * some 的函数签名 * @param {function} callback 回调函数 * callback 的函数签名 * @param {any} currentValue 当前元素 * @param {number} index 当前元素的索引 * @param {array} array 当前数组 * @return {boolean} 返回值为 true 时,停止循环,返回 true */ // some(callbackfn: (value: T, index: number, array: T[]) => unknown, thisArg?: any): boolean; // some 的用法 const result = arr.some((item, index, thisArr) => { console.log(item, index, thisArr); return item === 5; });
every
:测试是否所有的元素都通过了回调函数的测试。/** * every 的函数签名 * @param {function} callback 回调函数 * callback 的函数签名 * @param {any} currentValue 当前元素 * @param {number} index 当前元素的索引 * @param {array} array 当前数组 * @return {boolean} 返回值为 false 时,停止循环,返回 false */ // every(callbackfn: (value: T, index: number, array: T[]) => unknown, thisArg?: any): boolean; // every 的用法 const result = arr.every((item, index, thisArr) => { console.log(item, index, thisArr); return item === 5; });
some
和every
的两个的作用和用法都类似,不过他们的判定的结果是相反的,来看看他们是怎么中断forEach
的:
arr.some((item) => {
// some 的回调函数中,返回 true 时,停止循环,所以这里是大于才是 5%
if (Math.random() > 0.05) {
return true;
}
console.log(item);
return false;
})
arr.every((item) => {
// every 的回调函数中,返回 false 时,停止循环,和 some 相反,这里是小于才是 5%
if (Math.random() < 0.05) {
return false;
}
console.log(item);
return true;
})
find
和findIndex
老规矩先来认识一下这两个原型方法
find
:返回数组中第一个通过回调函数测试的项。/** * find 的函数签名 * @param {function} callback 回调函数 * callback 的函数签名 * @param {any} currentValue 当前元素 * @param {number} index 当前元素的索引 * @param {array} array 当前数组 * @return {boolean} 返回值为 true 时,停止循环,返回当前元素 */ // find(callbackfn: (value: T, index: number, array: T[]) => unknown, thisArg?: any): T | undefined; // find 的用法 const result = arr.find((item, index, thisArr) => { console.log(item, index, thisArr); return item === 5; });
findIndex
:返回数组中第一个通过回调函数测试项的索引。/** * findIndex 的函数签名 * @param {function} callback 回调函数 * callback 的函数签名 * @param {any} currentValue 当前元素 * @param {number} index 当前元素的索引 * @param {array} array 当前数组 * @return {boolean} 返回值为 true 时,停止循环,返回当前元素的索引 */ // findIndex(callbackfn: (value: T, index: number, array: T[]) => unknown, thisArg?: any): number; // findIndex 的用法 const result = arr.findIndex((item, index, thisArr) => { console.log(item, index, thisArr); return item === 5; });
find
和findIndex
的使用方法相同,不同的是返回结果一个是项,一个是索引,来看看他们是怎么中断forEach
的:
arr.find((item) => {
if (Math.random() < 0.05) {
return true;
}
console.log(item);
return false;
})
arr.findIndex((item) => {
if (Math.random() < 0.05) {
return true;
}
console.log(item);
return false;
})
好了,也就这么些方法可以中断数组的迭代了,以后面试再遇到这个问题,不敢怼面试官,那就上我这个标准答案,请不要再throw new Error()
了,真丢人。
forEach
和for
循环的区别
这个问题很多人觉得,forEach
是异步的,for
是同步的。
我信了你的个鬼,你输出forEach
的迭代项看看,有一次是乱序的吗?你有什么证据证明它是异步的?
哦对了,异步编程有一个重要的概念,就是回调函数,因为回调函数的执行时机不确定,所以这就算异步了?
好好看看我最开始写的forEach
和for
转换的例子,for
就是同步的,同步里面执行了一个方法,如果这个方法是异步的,那么它就是异步的,如果这个方法是同步的,那么它就是同步的。
不是所有带有回调函数都是异步编程,不信邪的在forEach
下面打印一个log
,看看是这个log
先输出还是forEach
回调函数里面的逻辑先执行。
给你讲讲真实的区别吧:
for in
、for of
不知道有没有人了解其中的原理;for in
语句以任意顺序迭代一个对象的除Symbol
以外的可枚举属性,包括继承的可枚举属性。
```
const obj = {
a: 'a',
b: 'b',
c: 'c',
[Symbol()]: 'symbol',
}
Object.defineProperty(obj, 'b', {
enumerable: false
})
for (let key in obj) {
console.log('obj.' + key + ' = 我是' + obj[key])
}
```
- `for of`是借助`Iterator`接口来迭代对象的。
```
const obj = {
a: 'a',
b: 'b',
c: 'c',
[Symbol.iterator]: function () {
const arr = ['a', 'b', 'c', 'd', 'e', 'f', 'g']
let index = 0
return {
next: () => {
return {
value: arr[index++],
done: index > arr.length
}
}
}
}
}
for (let item of obj) {
console.log(item)
}
```
普通的
for
循环,这个没什么好说的吧,就是定义一个结束循环的标志(和次数没关系,因为可以在循环体中重新设置用于判定的变量的值),每次循环之前都判定一下,确定结束了就不循环了。forEach
可以看到上面的for in
和for of
迭代的都是对象,forEach
作用在数组上面。
但是其实js
里面万物皆对象,你以为的数组其实也是对象,你可以试试[1,2,3]["1"]
看看值是什么,别说什么隐式类型转换,js
对象的key
到现在只有两种,一种是String
,一种是Symbol
,Map
的key
可以是对象,这里面要将的门道太多了,不在这次讨论的范畴。
那么他们的区别到底在什么地方,不说废话,直接上代码:
const arr = [1, , 3, , 5, , 7, , 9, 10];
arr.forEach((item) => {
console.log('这里是 forEach', item);
})
console.log('-------------------');
for (let i = 0; i < arr.length; i++) {
console.log('这里是 for i', arr[i]);
}
console.log('-------------------');
for (let i in arr) {
console.log('这里是 for in',arr[i]);
}
console.log('-------------------');
for (let i of arr) {
console.log('这里是 for of',i);
}
自己输出上面的示例看看结果吧,我这里就直接下结论了,上面的代码示例结果也能说明forEach
是同步的;
forEach
遇到遇到空位会跳过。- 普通的
for
循环一撸到底,肯定是不会跳过的。 for in
因为空位表现为undefined
,不是那种声明的undefined
,是没有这个玩意,可以通过Object.hasOwn(arr, 1)
来验证。for of
使用的是迭代器,也是直接一撸到底。
所以区别是什么?
- 写法不一样,
forEach
明显比for
循环少写很多代码,所以可以称之为简化版for
循环。 - 语法不一样,就拿中断来举例子,
forEach
中不能使用break
和continue
,return
在forEach
中的表现形式和在for
循环中的continue
相同。 - 迭代的元素有优化,
for i
和for of
都是一撸到底,for in
和forEach
表现相同,但是for in
迭代出来的key
类型是String
,只是表现相同,结果还是不同的。 - 用法不同,
forEach
使用的是回调函数,for
循环操作的直接就是变量。
分析完上面的一波,再来分析一下,如果对原数组对象在循环内部操作,对循环会有什么变化吧,直接上代码:
let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
arr.forEach((item, index) => {
if (index === 0) {
arr.shift();
}
console.log(item);
});
console.log('-------------------');
arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
for (let i = 0; i < arr.length; i++) {
if (i === 0) {
arr.shift();
}
console.log(arr[i]);
}
console.log('-------------------');
arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
for (let item of arr) {
if (item === 1) {
arr.shift();
}
console.log(item);
}
console.log('-------------------');
arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
for (let item in arr) {
if (item === '0') {
arr.shift();
}
console.log(arr[item]);
}
console.log('-------------------');
自己去测试一下结果,我还是直接上结论:
forEach
当前项不会改变,但是后面所有的项都会变化。for i
整个结果表现正确。for of
表现形式和forEach
相同,是不是侧面说明forEach
内部也是使用迭代器做的循环处理?for in
整个结果表现正确。
ok,到这我们不难发现,如果我们使用forEach
,然后在循环体操作原数组,那么结果是未知的,并且不能控制让结果正确,而使用for
循环,是可以让结果保持正确的,所以for
循环可以应用更多的复杂场景。
这个就探索到这里了,还有可以深入的地方,但是深入到这里已经够应付很多场景了。
forEach
的异步回调
本来是不想说异步的,但是上面提到了异步,这里也就简单的讲一下,es6
不是出了async
的异步标记,用来标识一个函数是异步的函数,那么在回调函数上面标记会发生什么呢?来试试:
let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const _await = (time) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, time);
});
}
arr.forEach(async (item, index) => {
// 这里模拟异步执行时间
await _await(Math.ceil(Math.random() * 100));
console.log(item);
});
结果中可以看到输出的顺序全乱了,如果你想正序的迭代一个数组,那么异步肯定是不可取的,这是不是又侧面的说明forEach
是同步的?
现在我有一个面试题,怎么保证上面的迭代能按照同步的方式进行打印出来?这个题就是给那些说forEach
是异步的人来打脸的,是异步的为什么你们从来都没解决过异步同步顺序执行呢?
总结
那些狗屁文章还有公众号不要再抄来抄去了,都不通过验证就直接抄,一个错误传遍整个圈子,大家还都说对。
还有那些什么面试题,面试造火箭,你火箭的螺丝型号都没搞清楚就敢开口问,完了之后错误的螺丝还就拧到火箭上了,火箭飞的磕磕盼盼的应该很爽吧。