整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

JavaScript中的forEach,你踩过哪些坑

JavaScript中的forEach,你踩过哪些坑?请避开这些常见误区

JavaScript的世界里,forEach是我们常用的数组遍历方法之一。大多数开发者都熟悉它的基础用法,但你知道吗?在处理异步操作时,forEach可能会让你掉进一些意想不到的“坑”。这篇文章将带你深入了解forEach的特性和局限,揭示一些你可能不知道的使用技巧和解决方案。无论你是前端新手,还是经验丰富的开发者,都能从中学到有用的知识,帮助你在实际项目中避开那些隐藏的陷阱。准备好了吗?让我们一探究竟!

先聊聊什么是forEach?

forEach是数组对象的一个原型方法,它会为数组中的每个元素执行一次给定的回调函数,并且总是返回undefined。不过需要注意的是,类似arguments这样的类数组对象是没有forEach方法的哦。

基本语法

arr.forEach(callback(currentValue [, index [, array]])[, thisArg])

别被这复杂的语法吓到,我们来逐个拆解。

参数详解

  1. callback:对每个元素执行的回调函数,它可以接受1到3个参数。
  • currentValue:当前处理的元素,必选。
  • index:当前处理元素的索引,可选。
  • array:正在操作的原数组对象,可选。
  1. thisArg:执行回调函数时this的值,默认为全局对象,可选。

1、forEach() 方法不支持处理异步函数

在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, 1end,但实际结果是 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 关键字,确保每个异步操作完成后才处理下一个元素,从而实现了按顺序输出。

2、异步函数中的错误无法被捕获

除了不能处理异步函数外,forEach还有另一个重要的限制:它无法捕获异步函数中的错误。这意味着即使异步函数在执行过程中抛出错误,forEach 仍然会继续进行下一个元素的处理,而不会对错误进行处理。这种行为可能会导致程序出现意外的错误和不稳定性。

3、无法中断或跳过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。

4、无法删除自身元素并重置索引

在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]

5、this 关键字的作用域问题

在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关键字指向正确的对象,从而正确访问对象的属性。

6、forEach 的性能低于 for 循环

forEach 方法虽然使用方便,但在性能方面却逊色于传统的 for 循环。原因在于 forEach 的函数签名包含参数和上下文,使得其性能低于 for 循环。

为什么 for 循环更快?

  1. 简单实现:for 循环的实现最为简单,没有额外的函数调用和上下文处理。
  2. 减少函数调用栈:forEach 方法每次迭代都会调用一次回调函数,增加了函数调用栈的开销。
  3. 上下文处理:forEach 方法需要处理函数的上下文和参数,这些操作都会消耗额外的时间和资源。

7、跳过已删除或未初始化的项

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'。

8、不会改变原数组

当调用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循环如何跳出循环呢?

听说视频配文档更容易理解,??

  1. 尝试使用break 和return
  2. MDN给出的官方解释
  3. 探究为什么break和return不行
  4. 如何变通跳出forEach循环
  5. MDN官方推荐的方法
  6. 其他方法

尝试使用break 和return

首先尝试一使用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给出的官方解释

为什么会出现这样的情况?先看一下官方文档的说明。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()


探究为什么break和return不行

先看看为什么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语句是不可以出现在函数体内的。


如何变通跳出forEach循环

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()


停forEach的三种方法

你很棒,但我得告诉你,至少有三种方法可以让JavaScript里的forEach停止。

1.抛个错误出来

找到第一个大于或等于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) {
  
}


哦!我的个神啊!我简直不敢相信,都快说不出话来。

2.把数组长度改成0

别这么惊讶,面试官跟我说。

咱们还可以通过把数组长度设置成0来让forEach打卡下班。你也知道,数组没了,forEach自然也就不跑了。

const array=[ -3, -2, -1, 0, 1, 2, 3 ]

array.forEach((it)=> {
  if (it >=0) {
    console.log(it)
    array.length=0
  }
})

哦!天哪,我的脑子都乱套了。

3.用splice法,砍掉数组的元素

这招和第二招一个味儿,如果能把目标元素后面的值都给删了,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
  }
})


感谢您的阅读!如果您对本文有任何疑问或想要分享您的看法,请随时通过私信或在下方评论区与我交流。