Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why array.forEach(() => { array.pop() }) would not empty the array

While on the nodejs REPL I was trying to clean up an array defined as const array = [...] and just found out that using array.forEach(() => /pop|shift/()) would not work. After such expression the array will still hold values in it.

I'm well aware of better methods to clean the array, like array.splice(0), but I'm really curious about this behavior as seems counter-intuitive, at least for me.

Here's the test:

const a = [1, 2, 3]

a.forEach(() => {
  a.shift()
})

console.log(a) // [ 3 ]

const b = [1, 2, 3]

b.forEach(() => {
  b.pop()
})

console.log(b) // prints [ 1 ]

Notes

  1. At first I was using arr.forEach(() => arr.pop()), so I though that one of the values was short-circuiting the forEach but wrapping the lambda in a body-block { .. } will also produce the same results.

  2. The results are consistent across different node versions and browsers .. so it seems like it's well-defined behavior.

  3. The quantity of leftover values, those still in the result array, change depending on the length of the input array, and seems to be Math.floor(array.length / 2)

  4. The leftover values are always ordered accordingly to the /pop|shift/ method used, so some of the calls are actually changing the input array.

  5. It also returns the same results by calling Array.prototype.forEach(array, fn)

like image 468
eridal Avatar asked Jan 07 '19 21:01

eridal


People also ask

Can you use forEach on an empty array?

The forEach() method calls a function for each element in an array. The forEach() method is not executed for empty elements.

What does Pop () method do when used with arrays?

The pop() method removes the last element from an array and returns that element.

How do you make an array empty?

Input : Array = [2, 3, 5, 4, 1] Output : Steps Taken: 3 Explanation: Step 1: Remove 5 and elements to its right so, Array becomes [2, 3] Step 2: Remove 3 as it is the maximum and right most already so, Array becomes [2] Step 3: Remove 2 and the array becomes EMPTY Hence, at the end of step 3 the array stands exhausted.


2 Answers

Check out this quote from here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach

If the values of existing elements of the array are changed, the value passed to callback will be the value at the time forEach() visits them; elements that are deleted before being visited are not visited.

You're iterating from the beginning and removing the last element each iteration. This means you're moving forward 1, and reducing the length by 1, each iteration. Hence why you're ending up with floor(initialLength / 2) iterations. You're modifying the same array that you're forEaching, which as stated above means that you will not have the callback invoked for those pop'd elements.

like image 95
John Avatar answered Sep 30 '22 20:09

John


Modifying an array while iterating over it is generally a bad idea. In fact, in Java, trying to do so would cause an exception to be thrown. But let's convert the forEach into an old-school for loop, and maybe you'll see the issue.

for (let i = 0; i < a.length; ++i) {
    a.pop();
}

Is it clearer now what's going on? Each iteration you're shortening the length of the array by 1 when you pop the last element off. So the loop will end after iterating over half the elements -- because by then, it will have REMOVED half the elements, too, causing the value of i to be more than the current length of the array.

The same thing is happening when you use forEach: you're shortening the array with each iteration when you pop, causing the loop to terminate after only half the elements have been iterated. In other words, the iterator variable will move forward past the end of the array as the array shrinks.

like image 29
IceMetalPunk Avatar answered Sep 30 '22 19:09

IceMetalPunk