No, you're not mad.
Just as you described, it is true that removing an item from the collection causes the next item to be skipped when the index is incremented. Also, the docs say that the forEach
callback is not invoked for index properties that have been deleted or are uninitialized (i.e. undefined
).
The Problem
To avoid skipping as the index is incremented, you might consider using a counter variable that you can increment each time you remove an item. Then each time you access an item, use the counter to shift the index backwards, like so:
var fruits = ["Banana", "Orange", "Apple", "Mango"]
function myFunction() {
var itemsRemoved = 0; // remember how many items you deleted
fruits.forEach(function(fruit, index, a) {
var shiftedIndex = index - itemsRemoved; // update the index
fruit = fruits[shiftedIndex];
if(fruit == "Banana" || fruit == "Orange") {
fruits.splice(shiftedIndex, 1); // item removed
itemsRemoved++; // update counter
}
})
}
myFunction();
// fruits: ['Apple', 'Mango']
This hack will remove Banana
and Orange
, however, it has a major drawback: The forEach
callback is only executed once per item in the array. Each time you remove an item, the array length is decremented by 1. So if you have 4 items and you remove 2 while iterating, then the forEach
loop will only run twice, skipping over the last two array items. This explains why Apple
will not be removed if you try to remove Banana
, Orange
and Apple
:
Loop 1:
- Executed: Yes
- Fruits: Banana, Orange, Apple, Mango
- Length: 4
- Remove: Index 0
- Length After Removal: 3
Loop 2:
- Executed: Yes
- Fruits: Orange, Apple, Mango
- Length: 3
- Remove: Index 0
- Length After Removal: 2
Loop 3:
- Executed: No
- Fruits: Apple, Mango
- Length: 2
- Remove: n/a
- Length After Removal: n/a
Loop 3 is never executed because by that point the loop has already run twice. It will not run a third time because after removing Banana
and Orange
the array length is now 2 and forEach
will only run once per array item.
Key Point
In general, when you remove an item from a collection while iterating over it at the same time, the results of the iteration are undefined and may produce strange behavior, ambiguous errors, and silent failures. Some languages even throw a concurrency exception under these circumstances.
Takeaway
The right approach here is to copy the items you want from the existing array over into a new array, ignoring the items you don't want. Then you can either discard the old array or overwrite it with the new array:
var fruits = ["Banana", "Orange", "Apple", "Mango"]
function myFunction() {
var newFruits = [];
fruits.forEach(function(fruit, index){
if(fruit != "Banana" && fruit != "Orange"){
newFruits.push(fruit);
}
})
fruits = newFruits;
}
myFunction();
// fruits: ['Apple', 'Mango']
To remove objects, the same idea applies, except you will be doing your checks against the object properties instead of strings:
var fruitObjects = [{name:"Banana"},{name:"Orange"},{name:"Apple"},{name:"Mango"}];
function removeFruitObjects(){
var newFruits = [];
fruitObjects.forEach(function(fruit, index){
if(fruit.name != "Banana" && fruit.name != "Orange"){
newFruits.push(fruit);
}
})
fruitObjects = newFruits;
}
removeFruitObjects();
// fruitObjects: [ { name: 'Apple' }, { name: 'Mango' } ]