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 thewords
reference points to later)- I've added the 3rd parameter
iterationArray
toforEach
which is the array that the iterator uses - I've also added a
console.log
inside the loop: note, thatiterationArray
will not change, butwords.length
will change (because you assign new arrays to it)
- I've added the 3rd parameter
- in the loop you create a new array using
words.filter
(e.g.['one', 'two', 'three', 'five', 'six', 'seven']
) and change thewords
variable to point to this new array - BUT this does not change the iterator that you have already created before: i.e. theforEach
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
- when the loop reaches the word "two" at index 1, you change the original array to
- 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 wordtrhee
which is now at index 1 which the iterator has already processed
- since you have altered the original array, the value at index 2 is now
- note, the
console.log
inside the loop: you can see that theiterationArray
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.