JavaScript的世界里,forEach是我们常用的数组遍历方法之一。大多数开发者都熟悉它的基础用法,但你知道吗?在处理异步操作时,forEach可能会让你掉进一些意想不到的“坑”。这篇文章将带你深入了解forEach的特性和局限,揭示一些你可能不知道的使用技巧和解决方案。无论你是前端新手,还是经验丰富的开发者,都能从中学到有用的知识,帮助你在实际项目中避开那些隐藏的陷阱。准备好了吗?让我们一探究竟!
forEach是数组对象的一个原型方法,它会为数组中的每个元素执行一次给定的回调函数,并且总是返回undefined。不过需要注意的是,类似arguments这样的类数组对象是没有forEach方法的哦。
基本语法
arr.forEach(callback(currentValue [, index [, array]])[, thisArg])
别被这复杂的语法吓到,我们来逐个拆解。
参数详解
在JavaScript中,forEach() 是一个同步方法,不支持处理异步函数。如果你在 forEach 中执行一个异步函数,forEach 不会等待异步函数完成,而是会立即处理下一个元素。这意味着如果你在 forEach 中使用异步函数,异步任务的执行顺序是无法保证的。
示例代码
async function test() {
let arr=[3, 2, 1];
arr.forEach(async item=> {
const res=await mockAsync(item);
console.log(res);
});
console.log('end');
}
function mockAsync(x) {
return new Promise((resolve, reject)=> {
setTimeout(()=> {
resolve(x);
}, 1000 * x);
});
}
test();
预期结果:
3
2
1
end
实际结果:
end
1
2
3
这个例子中,虽然我们希望按顺序输出 3, 2, 1 和 end,但实际结果是 end 先输出,然后才是 1, 2, 3。这是因为 forEach 不等待异步操作完成。
解决方法:使用 for...of 循环和 async/await
为了解决这个问题,我们可以使用 for...of 循环和 async/await 关键字来确保异步操作按顺序完成。
示例代码
async function test() {
let arr=[3, 2, 1];
for (let item of arr) {
const res=await mockAsync(item);
console.log(res);
}
console.log('end');
}
function mockAsync(x) {
return new Promise((resolve, reject)=> {
setTimeout(()=> {
resolve(x);
}, 1000 * x);
});
}
test();
输出结果:
3
2
1
end
在这个例子中,我们使用 for...of 循环代替 forEach 方法,通过在循环内部使用 await 关键字,确保每个异步操作完成后才处理下一个元素,从而实现了按顺序输出。
除了不能处理异步函数外,forEach还有另一个重要的限制:它无法捕获异步函数中的错误。这意味着即使异步函数在执行过程中抛出错误,forEach 仍然会继续进行下一个元素的处理,而不会对错误进行处理。这种行为可能会导致程序出现意外的错误和不稳定性。
除了无法处理异步函数和捕获错误之外,forEach还有一个限制:它不支持使用break或continue语句来中断或跳过循环。如果你需要在循环中途退出或跳过某个元素,应该使用其他支持这些语句的方法,例如for循环。
示例代码
let arr=[1, 2, 3];
try {
arr.forEach(item=> {
if (item===2) {
throw('error');
}
console.log(item);
});
} catch(e) {
console.log('e:', e);
}
// 输出结果:
// 1
// e: error
在这个例子中,我们尝试通过抛出异常来中断forEach循环。虽然这种方法在某些情况下有效,但并不是优雅或推荐的做法。
更好的解决方案:使用 for...of 循环
相比之下,for...of 循环更灵活,可以使用break和continue语句来控制循环的执行。
示例代码
let arr=[1, 2, 3];
for (let item of arr) {
if (item===2) {
break; // 中断循环
}
console.log(item);
}
// 输出结果:
// 1
在这个例子中,当遇到元素2时,循环会被中断,从而避免输出2和3。
在forEach中,我们无法控制索引的值,它只是盲目地递增直到超过数组的长度并退出循环。因此,删除自身元素以重置索引也是不可能的。来看一个简单的例子:
示例代码
let arr=[1, 2, 3, 4];
arr.forEach((item, index)=> {
console.log(item); // 输出: 1 2 3 4
index++;
});
在这个例子中,forEach遍历数组 arr,输出每个元素的值。虽然我们尝试在循环内部递增 index,但这并不会影响forEach的内部机制。forEach中的索引是自动管理的,并且在每次迭代时都会自动递增。
为什么无法删除元素并重置索引?
在forEach中,索引的值是由forEach方法内部控制的。即使我们手动修改索引变量,也不会影响forEach的遍历行为。更具体地说,当我们试图在forEach内部删除元素时,forEach不会重新计算索引,这会导致一些元素被跳过,或者某些情况下出现未定义的行为。
例如,如果我们尝试删除当前元素:
错误示范
let arr=[1, 2, 3, 4];
arr.forEach((item, index)=> {
if (item===2) {
arr.splice(index, 1); // 尝试删除元素2
}
console.log(item); // 输出: 1 2 4
});
console.log(arr); // 输出: [1, 3, 4]
在这个例子中,当我们删除元素2时,forEach并不会重置或调整索引,因此它继续处理原数组中的下一个元素。这导致元素3被跳过,因为原来的元素3现在变成了元素2的位置。
当元素 2 被删除后,原数组变为 [1, 3, 4],forEach会继续按照原索引顺序进行,因此输出 1, 2, 4,而元素 3 被跳过了。这是因为元素 3 在 2 被删除后移动到了索引 1 的位置,而forEach的索引已经移动到 2,所以直接输出了删除后的索引 2 位置的新元素 4。
更好的解决方案:使用for循环
let arr=[1, 2, 3, 4];
for (let i=0; i < arr.length; i++) {
if (arr[i]===2) {
arr.splice(i, 1); // 删除元素2
i--; // 调整索引
} else {
console.log(arr[i]); // 输出: 1 3 4
}
}
console.log(arr); // 输出: [1, 3, 4]
在forEach方法中,this关键字指的是调用该方法的对象。然而,当我们使用常规函数或箭头函数作为参数时,this关键字的作用域可能会出现问题。在箭头函数中,this关键字指的是定义该函数的对象;而在常规函数中,this关键字指的是调用该函数的对象。为了确保this关键字的正确作用域,我们可以使用bind方法来绑定函数的作用域。以下是一个说明this关键字作用域问题的例子:
示例代码
const obj={
name: "Alice",
friends: ["Bob", "Charlie", "Dave"],
printFriends: function () {
this.friends.forEach(function (friend) {
console.log(this.name + " is friends with " + friend);
});
},
};
obj.printFriends();
在这个例子中,我们定义了一个名为obj的对象,里面有一个printFriends方法。我们使用forEach方法遍历friends数组,并使用常规函数来打印每个朋友的名字和obj对象的name属性。然而,运行这段代码时,输出如下:
undefined is friends with Bob
undefined is friends with Charlie
undefined is friends with Dave
这是因为在forEach方法中使用常规函数时,该函数的作用域不是调用printFriends方法的对象,而是全局作用域。因此,无法访问obj对象的属性。
使用bind方法解决
为了解决这个问题,我们可以使用bind方法来绑定函数的作用域,将其绑定到obj对象。下面是一个使用bind方法解决问题的例子:
示例代码
const obj={
name: "Alice",
friends: ["Bob", "Charlie", "Dave"],
printFriends: function () {
this.friends.forEach(
function (friend) {
console.log(this.name + " is friends with " + friend);
}.bind(this) // 使用bind方法绑定函数的作用域
);
},
};
obj.printFriends();
运行这段代码,输出如下:
Alice is friends with Bob
Alice is friends with Charlie
Alice is friends with Dave
通过使用bind方法绑定函数的作用域,我们可以正确地访问obj对象的属性。
使用箭头函数解决
另一个解决方案是使用箭头函数。由于箭头函数没有自己的this,它会继承其当前作用域的this。因此,在箭头函数中,this关键字指的是定义该函数的对象。
示例代码
const obj={
name: "Alice",
friends: ["Bob", "Charlie", "Dave"],
printFriends: function () {
this.friends.forEach((friend)=> {
console.log(this.name + " is friends with " + friend);
});
},
};
obj.printFriends();
运行这段代码,输出如下:
Alice is friends with Bob
Alice is friends with Charlie
Alice is friends with Dave
使用箭头函数,我们可以确保this关键字指向正确的对象,从而正确访问对象的属性。
forEach 方法虽然使用方便,但在性能方面却逊色于传统的 for 循环。原因在于 forEach 的函数签名包含参数和上下文,使得其性能低于 for 循环。
为什么 for 循环更快?
forEach方法在遍历数组时会跳过未初始化的值和已删除的值。这可能会导致一些意想不到的行为。
跳过未初始化的值
在数组中,如果某些值未初始化,forEach会直接跳过这些值。来看下面这个例子:
const array=[1, 2, /* 空 */, 4];
let num=0;
array.forEach((ele)=> {
console.log(ele);
num++;
});
console.log("num:", num);
// 输出结果:
// 1
// 2
// 4
// num: 3
在这个例子中,数组中的第三个元素未初始化,forEach直接跳过了它。因此,虽然数组的长度是4,但实际被遍历的元素只有3个。
跳过已删除的值
当在forEach循环中删除数组元素时,forEach会跳过这些已删除的值。来看下面这个例子:
const words=['one', 'two', 'three', 'four'];
words.forEach((word)=> {
console.log(word);
if (word==='two') {
words.shift(); // 删除数组中的第一个元素 'one'
}
});
// 输出结果:
// one
// two
// four
console.log(words); // ['two', 'three', 'four']
在这个例子中,当遍历到元素 'two' 时,执行了 words.shift(),删除了数组中的第一个元素 'one'。由于数组元素向前移动,元素 'three' 被跳过,forEach 直接处理新的第三个元素 'four'。
当调用forEach方法时,它不会改变原数组,即它被调用的数组。然而,传递的回调函数可能会改变数组中的对象。
示例代码1
const array=[1, 2, 3, 4];
array.forEach(ele=> { ele=ele * 3 })
console.log(array); // [1, 2, 3, 4]
在这个例子中,forEach方法并没有改变原数组。虽然在回调函数中对每个元素进行了乘3的操作,但这些操作并没有反映在原数组中。
如果希望通过forEach改变原数组,需要直接修改数组元素的值,而不是简单地对元素进行赋值。
示例代码
const numArr=[33, 4, 55];
numArr.forEach((ele, index, arr)=> {
if (ele===33) {
arr[index]=999;
}
});
console.log(numArr); // [999, 4, 55]
在这个例子中,我们通过forEach方法直接修改了数组中的元素,从而改变了原数组。
示例代码2
const changeItemArr=[{
name: 'wxw',
age: 22
}, {
name: 'wxw2',
age: 33
}];
changeItemArr.forEach(ele=> {
if (ele.name==='wxw2') {
ele={
name: 'change',
age: 77
};
}
});
console.log(changeItemArr); // [{name: "wxw", age: 22}, {name: "wxw2", age: 33}]
在这个例子中,尝试对数组中的对象进行替换操作,但这种方式并不会改变原数组中的对象。
解决方案:通过索引改变数组中的对象
为了正确替换数组中的对象,可以通过索引来直接修改数组中的对象。
示例代码
const allChangeArr=[{
name: 'wxw',
age: 22
}, {
name: 'wxw2',
age: 33
}];
allChangeArr.forEach((ele, index, arr)=> {
if (ele.name==='wxw2') {
arr[index]={
name: 'change',
age: 77
};
}
});
console.log(allChangeArr); // [{name: "wxw", age: 22}, {name: "change", age: 77}]
在这个例子中,通过索引直接修改数组中的对象,从而实现了对原数组的修改。
总结一下,forEach虽然方便,但在一些特定场景下,使用传统的for循环或其他遍历方法可能更适合你的需求。比如,当你需要精确控制循环流程、处理异步操作或是修改原数组时,for循环往往能提供更高的灵活性和性能。
使用for循环的时候可以使用break 或者return语句来结束for循环(return直接结束函数),但是如果使用forEach循环如何跳出循环呢?
听说视频配文档更容易理解,??
首先尝试一使用return语句----木有效果
[1,2,3,4,5].forEach(item=>{ if(item===2){ return } console.log(item); })
在尝试一下使用break语句----报错
[1,2,3,4,5].forEach(item=>{ if(item===2){ break } console.log(item); })
为什么会出现这样的情况?先看一下官方文档的说明。MDN文档上明确说明forEach循环是不可以退出的。
引自MDN
There is no way to stop or break a forEach() loop other than by throwing an exception. If you need such behavior, the forEach() method is the wrong tool.
注意: 没有办法中止或者跳出 forEach() 循环,除了抛出一个异常。如果你需要这样,使用 forEach() 方法是错误的。
若你需要提前终止循环,你可以使用:
简单循环
for...of 循环
Array.prototype.every()
Array.prototype.some()
Array.prototype.find()
Array.prototype.findIndex()
先看看为什么return没有效果,break报错,forEach的实现方式用代码表示出来可以写成如下的结构
const arr=[1, 2, 3, 4, 5]; for (let i=0; i < arr.length; i++) { const rs=(function(item) { console.log(item); if (item > 2) return false; })(arr[i]) }
使用return语句相当于在每个自执行函数中将返回值复制给rs,但是实际对整个函数并没有影响。而使用break语句报错是因为再JS的解释器中break语句是不可以出现在函数体内的。
MDN官方推荐的方法
every在碰到return false的时候,中止循环。some在碰到return ture的时候,中止循环。 var a=[1, 2, 3, 4, 5] a.every(item=>{ console.log(item); //输出:1,2 if (item===2) { return false } else { return true } }) var a=[1, 2, 3, 4, 5] a.some(item=> { console.log(item); //输出:1,2 if (item===2) { return true } else { return false } })
其他方法
1.使用for循环或者for in 循环代替
2.使用throw抛出异常
try { [1, 2, 3, 4, 5].forEach(function(item) { if (item===2) throw item; console.log(item); }); } catch (e) {}
3.使用判断跑空循环
var tag; [1, 2, 3, 4, 5].forEach(function(item){ if(!tag){ console.log(item); if(item===2){ tag=true; } } })
这样做有两个问题,第一个问题,全局增加了一个tag变量,第二个问题,表面上看是终止了forEach循环,但是实际上循环的次数并没有改变,只是在不满足条件的时候callback什么都没执行而已,先来解决第一个问题,如何删除全局下新增的tag变量 。实际上forEach还有第二个参数,表示callback的执行上下文,也就是在callback里面this对应的值。因此我们可以讲上下文设置成空对象,这个对象自然没有tag属性,因此访问this.tag的时候会得到undefined
[1, 2, 3, 4, 5].forEach(function(item){ if(!this.tag){ console.log(item); if(item===2){ this.tag=true; } } },{})
4.修改索引
var array=[1, 2, 3, 4, 5] array.forEach(item=>{ if (item==2) { array=array.splice(0); } console.log(item); })
讲解:
forEach的执行细节
1.遍历的范围在第一次执行callback的时候就已经确定,所以在执行过程中去push内容,并不会影响遍历的次数,这和for循环有很大区别,下面的两个案例一个会造成死循环一个不会
var arr=[1,2,3,4,5] //会造成死循环的代码 for(var i=0;i<arr.length;i++){ arr.push('a') } //不会造成死循环 arr.forEach(item=>arr.push('a'))
2.如果已经存在的值被改变,则传递给 callback 的值是 forEach 遍历到他们那一刻的值。
var arr=[1,2,3,4,5]; arr.forEach((item,index)=>{ console.log(`time ${index}`) arr[index+1]=`${index}a`; console.log(item) })
3.已删除的项不会被遍历到。如果已访问的元素在迭代时被删除了(例如使用 shift()),之后的元素将被跳过。
var arr=[1,2,3,4,5]; arr.forEach((item,index)=>{ console.log(item) if(item===2){ arr.length=index; } })
在满足条件的时候将后面的值截掉,下次循环的时候照不到对应的值,循环就结束了,但是这样操作会破坏原始的数据,因此我们可以使用一个小技巧,即将数组从0开始截断,然后重新赋值给数组也就是array=array.splice(0)
本期教程资料请点击更多下载,提取码: 558x
良心教程,欢迎关注评论,或者访问我们的官网http://www.bingshangroup.com
和我们交流。
当时就直接回他:“不能,我做不到。”
结果呢,这句话就像按了快进键,面试官突然宣布面试结束。
心里那个郁闷啊,我就问面试官:“这有啥不对吗?难道真的有办法在JavaScript中让forEach歇菜吗?”
还没等他回我,我就开始自我解惑,说出了我认为forEach不能停的理由。
我的小伙伴们,猜猜看,下面这段代码会打印出什么数字?
会只打印一个数字,还是一串数字?
正确答案是,它会打印出‘0’、‘1’、‘2’、‘3’。
const array=[ -3, -2, -1, 0, 1, 2, 3 ]
array.forEach((it)=> {
if (it >=0) {
console.log(it)
return // or break
}
})
是的!我把这代码拿给面试官看,但他还是坚持认为JavaScript的forEach循环是可以停的。
哦天啊,开什么国际玩笑呢。
要让他信服,我就得再来一次forEach的模拟。
Array.prototype.forEach2=function (callback, thisCtx) {
if (typeof callback !=='function') {
throw `${callback} is not a function`
}
const length=this.length
let i=0
while (i < length) {
if (this.hasOwnProperty(i)) {
// Note here:Each callback function will be executed once
callback.call(thisCtx, this[ i ], i, this)
}
i++
}
}
确实,当我们用forEach遍历数组时,每个元素都要跑一遍回调函数,早退门都没有。
比如说,下面这段代码里,就算func1遇到了break,控制台还是会打印出‘2’。
const func1=()=> {
console.log(1)
return
}
const func2=()=> {
func1()
console.log(2)
}
func2()
你很棒,但我得告诉你,至少有三种方法可以让JavaScript里的forEach停止。
找到第一个大于或等于0的数字后,代码就进入死胡同了。所以控制台只会跟你说个0。
const array=[ -3, -2, -1, 0, 1, 2, 3 ]
try {
array.forEach((it)=> {
if (it >=0) {
console.log(it)
throw Error(`We've found the target element.`)
}
})
} catch (err) {
}
哦!我的个神啊!我简直不敢相信,都快说不出话来。
别这么惊讶,面试官跟我说。
咱们还可以通过把数组长度设置成0来让forEach打卡下班。你也知道,数组没了,forEach自然也就不跑了。
const array=[ -3, -2, -1, 0, 1, 2, 3 ]
array.forEach((it)=> {
if (it >=0) {
console.log(it)
array.length=0
}
})
哦!天哪,我的脑子都乱套了。
这招和第二招一个味儿,如果能把目标元素后面的值都给删了,forEach也就自动停工了。
const array=[ -3, -2, -1, 0, 1, 2, 3 ]
array.forEach((it, i)=> {
if (it >=0) {
console.log(it)
// Notice the sinful line of code
array.splice(i + 1, array.length - i)
}
})
我瞪大了眼睛,真不想看这代码。太伤眼了。
最后我跟面试官说:“哎,可能你说的对,你确实找到了停forEach的方法,但要是用这种代码,我觉得你们老板迟早得让你走人。”
或许咱们应该考虑用for循环或者some方法来解决问题。
1. for
const array=[ -3, -2, -1, 0, 1, 2, 3 ]
for (let i=0, len=array.length; i < len; i++) {
if (array[ i ] >=0) {
console.log(array[ i ])
break
}
}
2. some
const array=[ -3, -2, -1, 0, 1, 2, 3 ]
array.some((it, i)=> {
if (it >=0) {
console.log(it)
return true
}
})
感谢您的阅读!如果您对本文有任何疑问或想要分享您的看法,请随时通过私信或在下方评论区与我交流。
*请认真填写需求信息,我们会在24小时内与您取得联系。