Let's clear up one point: in the initial example provided by the OP, the deletion actually affects the third element of the array, not the second as indicated by the comment. It seems that the intention was to remove the second element based on the subsequent example.
To correct this issue, we can utilize the index parameter passed to the forEach callback like so:
let arr = [1, 2, 3, 4];
arr.forEach((el, index) => {
console.log(el);
if (el === 2) {
// deleting the actual second element instead of the one at index 2
arr.splice(index, 1);
}
});
Interestingly, even after fixing the semantic error, the console will still display what was mentioned by the OP:
1
2
4
This occurs despite the resulting value of arr
being updated to [1, 3, 4]
due to the splice()
operation.
So why does this happen? The Mozilla Developer Network (MDN) provides a similar scenario regarding altering an array within a forEach loop. Essentially, the callback function passed to forEach
is executed for every index of the array until it reaches the end. During the second iteration, the logic refers to index 1 and removes that index, causing elements following it to shift down one position: the value 3
moves to index 1, while 4
takes the spot at index 2. As we have already processed index 1, the third iteration occurs at index 2, now containing the value 4
.
The table below illustrates this process further:
Iteration |
Value of el |
Initial arr state |
Final arr state |
1 |
1 |
[1, 2, 3, 4] |
[1, 2, 3, 4] |
2 |
2 |
[1, 2, 3, 4] |
[1, 3, 4] |
3 |
4 |
[1, 3, 4] |
[1, 3, 4] |
In essence, you can think of Array.prototype.forEach
as behaving similarly to the following:
Array.prototype.forEach = function(callbackFn, thisArg) {
for (let index = 0; index < this.length; ++index) {
callbackFn.call(thisArg, this.at(index), index, this)
}
}
The distinction between the two examples lies in how the objects are altered. In the first instance, the OP uses splice
to directly modify the object referenced by the variable arr
("in place", per the MDN documentation). Conversely, the second example involves reassigning the variable arr
to point at a new object. However, because forEach
operates as a function, the original object pointed to by arr
remains accessible within the closure formed by the forEach
call until the function finishes. This becomes more apparent when additional logging is introduced using the third parameter passed to the callback (the array on which the callback operated).
let arr = [1,2,3,4];
arr.forEach((el, index, list) => {
console.log("el:", el, "list:", list, "arr:", arr);
if (el === 2) {
arr = arr.filter(el => el !== 2); // removing element 2
}
});
The adjusted code snippet yields the following results:
el: 1 list: [1, 2, 3, 4] arr: [1, 2, 3, 4]
el: 2 list: [1, 2, 3, 4] arr: [1, 2, 3, 4]
el: 3 list: [1, 2, 3, 4] arr: [1, 3, 4]
el: 4 list: [1, 2, 3, 4] arr: [1, 3, 4]
Notice that the value of list
, obtained from the forEach
closure, remains constant despite arr
undergoing updates during the second traversal.