Why is this JavaScript map NOT an Infinite Loop?
Asked Answered
C

3

8

I'm learning JavaScript. I wrote this code to learn the map function. But then I got confused as to why is this not mapping over it continuously as with each map sequence a new element is pushed to the array. Shouldn't it continue to push new elements as it is mapping over ? Why does the map function only run for the original three elements and not for the new pushed ones?

I tried to debug it in the node environment and the arr variable goes in a closure. I know what a closure but I'm not able to understand what is going on here.

let array = [1, 2, 3];

array.map((element) => {
  array.push(10);
  console.log(element);
});

I expect that the output should be 1,2,3,10,10,10,10,10,10,10,10......10

But the actual output is only 1,2,3.

Chaumont answered 11/6, 2019 at 14:14 Comment(8)
Where do you see that result? When I run your code, I see just 1, 2, 3, as expected because of how .map() is specified to behaveLesslie
In your original code, you referenced arr, which isn't defined; I assumed you meant array.Lesslie
Modifying the array during iteration does not modify the iteration of map. From the spec Pointy mentioned: The range of elements processed by map is set before the first invocation of callback. Elements which are appended to the array after the call to map begins will not be visited by callback. Any
If you need infinity, you can use for loopAnaximenes
I wrote the wrong output. It is only 1 2 3 in the console. But my question still remains the same. In the callback function I'm mutating the array to have more elements with each iteration.Chaumont
I also see in the debuggin console that the "arr" is in "closure". Can someone explain to me if that has anything to do with this behaviour.Chaumont
@Chaumont no, it doesn't.Notable
Wow, it's interesting. It's definitely related to language implementation. The same logical code in Python would cause an infinite loop, as normal people would expect.Antisocial
B
10

To quote from MDN:

The range of elements processed by map is set before the first invocation of callback. Elements which are appended to the array after the call to map begins will not be visited by callback. If existing elements of the array are changed, their value as passed to callback will be the value at the time map visits them.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map#Description

So, it behaves like that because that is how it is designed. And it is designed that way, amongst other reasons, to prevent infinite loops!

map is a function from the Functional Programming world, where immutability is an important principle. According to this principle, if you call map on an input (and other variables don't change) you will always get exactly the same result. Allowing modification of the input breaks immutability.

Blacken answered 11/6, 2019 at 14:21 Comment(4)
How is the range stored. I mean is it stored in the scope of the map function or globally?Chaumont
@Chaumont it's maintained internally by the implementation of .map()Lesslie
I also see in the debugging console that the "arr" is in "closure". Can you explain to me if that has anything to do with this behaviour.Chaumont
I think the closure (which is the environment created by the arrow function) has no connection to the behaviour in this question. All you need to know is that map applies the function to each element of itself and returns the result. The specification of map doesn't specify how it does that, and it's probably more useful to understand the specification (i.e. how it should behave) than the internal implementation.Blacken
A
7

Because it does not mutate the array (see Array​.prototype​.map()).

Instead, it returns a new array with the results of calling a provided function on every element in the calling array.

In the following snippet, what mutates the array is the call to array.push(10); three times (once per element in the original array) and not the map function itself.

let newArray = array.map((element) => {
    array.push(10); // <-- here you mutate the array
    console.log(element);
});

An important quote from the mentioned documentation (and key point here is):

The range of elements processed by map is set before the first invocation of callback. Elements which are appended to the array after the call to map begins will not be visited by callback.


In the following snippet you can see an example of how to properly use the map function:

let array = [1,2,3];
let newArray = array.map(element => element + 10);  // sum 10 to every element

console.log('original: ', array); // original:  [1,2,3]
console.log('new one: ', newArray) // new one:  [11,12,13]

One last thought (from the docs too), taking as reference the code you posted:

Since map builds a new array, using it when you aren't using the returned array is an anti-pattern; use forEach or for-of instead.

Signs you shouldn't be using map:

  • A) You're not using the array it returns, and/or

  • B) You're not returning a value from the callback.

Assistance answered 11/6, 2019 at 14:23 Comment(2)
does it have any performance impact? Using map instead of forEach or for-of. Does the newly created array take up memory even if we are not assigning it as is the case in the original question?Chaumont
does it have any performance impact ... well, I'm not sure about it, we would have to perform some benchmark tests in order to measure it but here is a related post . Does the newly created array take up memory even if we are not assigning it (...)? .... well, if you don't use the value at all, it is supposed to be Garbage Collected (in case it takes some memory at some point, internally by the map function), so, no, it is not supposed to take up memory.Assistance
L
4

Why is exactly as seen on MDN:

var new_array = arr.map(function callback(currentValue[, index[, array]]){}

The range of elements processed by map is set before the first invocation of callback. Elements which are appended to the array after the call to map begins will not be visited by callback.

(Thank you to Joe's post above from MDN for this quote.)

Once map is called it then takes the array at that moment as it's parameter; once it's been passed then any changes are irrelevant to the previous variable itself.

See below:

let array = [1, 2, 3];

array.map((element) => {
  array.push(10);
  console.log(element);
});

console.log(array)
Labrecque answered 11/6, 2019 at 14:22 Comment(1)
I don't see how either the method signature or the parameter not being a reference has anything to do with this behaviour.Notable

© 2022 - 2024 — McMap. All rights reserved.