'JS - filter v.s. splice in forEach loop

Background knowledge

We can mutate an array using splice and filter. filter is not a mutable method, but splice is a mutable method. So if we use splice to an array, then the original array will be mutated.

The problem

Now, we are going to do forEach to an array and if a condition matches, we will remove an element using splice and filter. Let's see:

1. splice

let arr = [1, 2, 3, 4];

arr.forEach(el => {
  console.log(el);
  
  if (el === 2) {
    arr.splice(el, 1); // remove element 2
  }
});

Yeah that's what we expect. That prints 1, 2, 4. Because in the middle of the loop we mutate the arr, the result is broken.

2. filter

let arr = [1,2,3,4];

arr.forEach(el => {
  console.log(el);

  if (el === 2) {
    arr = arr.filter(el => el !== 2); // remove element 2
  }
});

However, as you can see, that prints 1, 2, 3, 4 even if we mutate the arr in the middle of the loop!

The question

We've mutated two arrays in the middle of the loop in a similar way, but the results are different! What happened?

Why does splice affect original array but filter not?



Solution 1:[1]

One point of clarification: in the OP's first example he's actually deleting the third value in the array rather than the second one despite the comment indicating he meant to delete the second element (at least, that is what I think he was going for based on the subsequent example).

One would fix that problem by using the second parameter passed to the forEach callback as follows:

let arr = [1, 2, 3, 4];

arr.forEach((el, index) => {
  console.log(el);
  
  if (el === 2) {
    // actually remove the second element instead of the element at index 2
    arr.splice(index, 1);
  }
});

What I find interesting is that even if the semantic error is fixed, the console will still show what the OP mentioned:

1
2
4

This, despite the resulting value of arr being set to [1, 3, 4] by the splice() call.

What happened? The MDN has a similar example regarding modifying an array inside a forEach loop. Basically, the callback one passes to forEach is invoked for every index of the list until it reaches the length of the list. In the second invocation, the underlying logic is pointing to index 1 and the callback deletes that index, moving everything currently following that index forward one index in the array: the value 3 is moved to index 1 and the value 4 is moved to index 2. Because we've already iterated over index 1, the third invocation will be invoked on index 2 which now contains the value 4.

The following table is another way to see this:

Iteration value of el arr value before callback arr value after callback
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]

Basically, you can think of Array.prototype.forEach being defined 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 difference between the two examples is that in the first one, the OP is using splice to modify the object referenced by the variable arr "in place" (as noted in the MDN doc). In the second example, the OP is changing the variable arr to point to a new object; however, because forEach is a function, the original object referenced by arr will be kept in scope by the forEach closure until the function completes. This becomes a little easier to see when you add a little more logging to the example using the third parameter passed to the callback (the array against which the callback was executed).

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); // remove element 2
  }
});

This modified example will produce the following output:

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]

One can see that the value of list, which comes from the forEach closure, never changes despite arr getting overwritten during the second iteration.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Coren