Home > other >  Why removing element have odd behavior in forEach in JavaScript?
Why removing element have odd behavior in forEach in JavaScript?

Time:07-26

I am trying to remove a word from an array based on an index which doesn't exist in another array, but I am observing odd behavior when I use the splice and filter methods.

Can anyone explain the scenario below? Why is it happening like this in both cases, even though the same object is being altered on iteration?

Words

['one', 'two', 'three', 'four', 'five', 'six', 'seven']

Removable Words

['four', 'two', 'eight']

let words = ['one', 'two', 'three', 'four', 'five', 'six', 'seven'];
let removedWords = ['four', 'two', 'eight'];

words.forEach((word) => {
  console.log(word);
  if (removedWords.includes(word)) {
    words = words.filter((removableWord) => removableWord !== word)
  }
});

/* Output */
//one
//two
//three
//four
//five
//six
//seven

let words = ['one', 'two', 'three', 'four', 'five', 'six', 'seven'];
let removedWords = ['four', 'two', 'eight'];
words.forEach((word, index) => {
  console.log(word);
  if (removedWords.includes(word)) {
    words.splice(index, 1);
  }
});

/* Output */
//one
//two
//four
//six
//seven

As mentioned in this Mozila document forEach() does not make a copy of the array before iterating. Shouldn't it behave the same as splice after filtering and assigning back to the original object?

Note: Just to add on this, the splice method makes changes on original array and the filter method creates a new copy of the array and doesn't alter the original array, but in the given example (after filtering), the result is assigned back to the original array.

CodePudding user response:

Your first example also works, but your console.log may confuse you. You log once for every word in the for loop before filtering.
Just log the result words after the loop to see that it works.

let words = ['one', 'two', 'three', 'four', 'five', 'six', 'seven'];
let removedWords = ['four', 'two', 'eight'];

words.forEach((word, index, iterationArray) => {
  console.log(index, word, iterationArray.length, words.length);
  if (removedWords.includes(word)) {
    words = words.filter((removableWord) => removableWord !== word)
  }
});

console.log(words);

Answer to the OPs comment:
So I guess, that you are confused by

"forEach() does not make a copy of the array before iterating"

  • it is true, that forEach() does not make a copy: but you make a copy inside the loop
  • at the start the variable words is a reference to the original array ['one', 'two', 'three', 'four', 'five', 'six', 'seven']
  • now you call words.forEach() which is a function that returns an iterator on this array (The iterator will always point to this original array, no matter if you change where the words reference points to later)
    • I've added the 3rd parameter iterationArray to forEach which is the array that the iterator uses
    • I've also added a console.log inside the loop: note, that iterationArray will not change, but words.length will change (because you assign new arrays to it)
  • in the loop you create a new array using words.filter (e.g. ['one', 'two', 'three', 'five', 'six', 'seven']) and change the words variable to point to this new array - BUT this does not change the iterator that you have already created before: i.e. the forEach loop still "points" to the original array

For the 2nd example:

let words = ['one', 'two', 'three', 'four', 'five', 'six', 'seven'];
let removedWords = ['four', 'two', 'eight'];
words.forEach((word, index, iterationArray) => {
  console.log(word, index, iterationArray.length);
  if (removedWords.includes(word)) {
    words.splice(index, 1);
  }
});
console.log(words);

  • again, forEach will not make a copy
  • the iterator will point to the original array
  • but now you change the original array, inside of your loop: i.e.
    • when the loop reaches the word "two" at index 1, you change the original array to ['one', 'three', 'four', 'five', 'six', 'seven']
    • in other words: you delete the item at index 1 and the other items are shifted to the left
  • now the iterator will continue and the next index is 2
    • since you have altered the original array, the value at index 2 is now four (and you missed the word trhee which is now at index 1 which the iterator has already processed
  • note, the console.log inside the loop: you can see that the iterationArray is changed

CodePudding user response:

When you say let words = <an array> the reference to that array object (say ref1) is stored in the variable words. When you call forEach on that reference (ref1), it will keep referring to that reference perpetually.

Inside the loop, after filtering, you are getting a new filtered array which is a different array in memory. You may use the same words variable to hold the reference (ref2 / ref3), but this doesn't change the one on which forEach is acting on.

However, when you use splice, the original array edits itself.

Note: Not only that, you are producing 2 different filtered arrays successively with each call to filter.

['one', 'two', 'three', 'five', 'six', 'seven']
['one', 'three', 'five', 'six', 'seven']

Eventually, your first method works and produces the desired result, but you create 'X' copies of arrays if you have X items to be removed from words.

Your second method is better on performance because it doesn't keep producing copies of the array for each removal, but you have to think about which index you are removing, and how forEach will continue after an index is removed.

In either case, your console log is placed in the wrong place.

  • Related