To be fair, .map() is a non-destructive method. However, the callback function you pass to .map() can make it destructive.
Array.prototype.map()
First, let's do a quick review. The .map() method iterates over an array, passes each element to a given callback function, and then puts the return value in a new array at the element's index location. For example:
const originalArray = [2, 3, 4];
const mappedArray = originalArray.map(x => x ** 2);
console.log('originalArray = ', originalArray)
console.log('mappedArray = ', mappedArray)
mappedArray[0] = 10;
console.log('originalArray = ', originalArray)
console.log('mappedArray = ', mappedArray)
// ♥ node map_examples.js
originalArray = [ 2, 3, 4 ]
mappedArray = [ 4, 9, 16 ]
originalArray = [ 2, 3, 4 ]
mappedArray = [ 10, 9, 16 ]
In this example we can see that mappedArray truly is a new array and not a new pointer to the original array. Not only does originalArray contain the same values after the .map() call, but it also remains unchanged after we set mappedArray at index 0 to 10.
Array.prototype.map() on an Array of Objects
However, what if .map() is called on is an array of objects? Let's say we want to copy of an array of objects and update one of the key value pairs. You also want the original array of objects to remain unchanged. Given our understanding that .map() is non-destructive, we may write our javascript solution something like this:
const objectsArray = [
{'id': 1, 'name': 'Erik', 'yearsCompleted': 2, 'status': 'student'},
{'id': 2, 'name': 'Carol', 'yearsCompleted': 1, 'status': 'student'},
{'id': 3, 'name': 'Sarah', 'yearsCompleted': 4, 'status': 'student'}
];
const mappedObjectsArray = objectsArray.map(student => {
if (student.yearsCompleted === 4) student.status = 'graduated';
return student
})
console.log('objectsArray = ', objectsArray)
console.log('mappedObjectsArray = ', mappedObjectsArray)
// ♥ node map_examples.js
objectsArray = [
{ id: 1, name: 'Erik', yearsCompleted: 2, status: 'student' },
{ id: 2, name: 'Carol', yearsCompleted: 1, status: 'student' },
{ id: 3, name: 'Sarah', yearsCompleted: 4, status: 'graduated' }
]
mappedObjectsArray = [
{ id: 1, name: 'Erik', yearsCompleted: 2, status: 'student' },
{ id: 2, name: 'Carol', yearsCompleted: 1, status: 'student' },
{ id: 3, name: 'Sarah', yearsCompleted: 4, status: 'graduated' }
]
Uhhh, that's not what we expect. Looking back at the code you might be thinking, "I know what we did wrong. The parameter points points to the actual object in objectsArray. We need to create a new object, modify it and return that new copy." Great idea. Let's try it:
const objectsArray = [
{'id': 1, 'name': 'Erik', 'yearsCompleted': 2, 'status': 'student'},
{'id': 2, 'name': 'Carol', 'yearsCompleted': 1, 'status': 'student'},
{'id': 3, 'name': 'Sarah', 'yearsCompleted': 4, 'status': 'student'}
];
const mappedObjectsArray = objectsArray.map(student => student.yearsCompleted === 4 ? {...student, 'status': 'graduated'} : student)
console.log('objectsArray = ', objectsArray)
console.log('mappedObjectsArray = ', mappedObjectsArray)
mappedObjectsArray[0].status = 'sophmore';
console.log('objectsArray = ', objectsArray)
console.log('mappedObjectsArray = ', mappedObjectsArray)
// ♥ node map_examples.js
objectsArray = [
{ id: 1, name: 'Erik', yearsCompleted: 2, status: 'student' },
{ id: 2, name: 'Carol', yearsCompleted: 1, status: 'student' },
{ id: 3, name: 'Sarah', yearsCompleted: 4, status: 'student' }
]
mappedObjectsArray = [
{ id: 1, name: 'Erik', yearsCompleted: 2, status: 'student' },
{ id: 2, name: 'Carol', yearsCompleted: 1, status: 'student' },
{ id: 3, name: 'Sarah', yearsCompleted: 4, status: 'graduated' }
]
objectsArray = [
{ id: 1, name: 'Erik', yearsCompleted: 2, status: 'sophmore' },
{ id: 2, name: 'Carol', yearsCompleted: 1, status: 'student' },
{ id: 3, name: 'Sarah', yearsCompleted: 4, status: 'student' }
]
mappedObjectsArray = [
{ id: 1, name: 'Erik', yearsCompleted: 2, status: 'sophmore' },
{ id: 2, name: 'Carol', yearsCompleted: 1, status: 'student' },
{ id: 3, name: 'Sarah', yearsCompleted: 4, status: 'graduated' }
]
Well that fixed our first unexpected behavior, but now we have a new one to deal with. Sarah's student record remained unchanged in the original array, but look at the original array after we modified Erik's status to 'sophmore' in mappedObjectsArray. Erik's status in objectsArray was also modified.
This is because javascript is not storing the actual object at each index of objectsArray, it stores a pointer that references the location in memory where the object is stored. So the .map() is creating a new array, but it is filling it with pointers to the same objects.
To fix this we need to instantiate new objects for every index location in mappedObjectsArray. The following shows an example of how to do this:
const objectsArray = [
{'id': 1, 'name': 'Erik', 'yearsCompleted': 2, 'status': 'student'},
{'id': 2, 'name': 'Carol', 'yearsCompleted': 1, 'status': 'student'},
{'id': 3, 'name': 'Sarah', 'yearsCompleted': 4, 'status': 'student'}
];
const mappedObjectsArray = objectsArray.map(student => student.yearsCompleted === 4 ? {...student, 'status': 'graduated'} : {...student})
mappedObjectsArray[0].status = 'sophmore';
console.log('objectsArray = ', objectsArray)
console.log('mappedObjectsArray = ', mappedObjectsArray)
// ♥ node map_examples.js
objectsArray = [
{ id: 1, name: 'Erik', yearsCompleted: 2, status: 'student' },
{ id: 2, name: 'Carol', yearsCompleted: 1, status: 'student' },
{ id: 3, name: 'Sarah', yearsCompleted: 4, status: 'student' }
]
mappedObjectsArray = [
{ id: 1, name: 'Erik', yearsCompleted: 2, status: 'sophmore' },
{ id: 2, name: 'Carol', yearsCompleted: 1, status: 'student' },
{ id: 3, name: 'Sarah', yearsCompleted: 4, status: 'graduated' }
]
Now we're getting the behavior that we expected on our first attempt. Because we created new objects for each index location of mappedObjectsArray, when we modified these new objects, the objects in objectsArray remain unchanged.
Overtly Destructive Callback Functions
Let's look at one more example to solidify the idea that the callback function you pass in can make .map() destructive.
originalArray = [1, 2, 3];
mappedArray = originalArray.map((x, index)=> originalArray[index] = x * 2);
console.log('originalArray = ', originalArray)
console.log('mappedArray = ', mappedArray)
// ♥ node map_examples.js
originalArray = [ 2, 4, 6 ]
mappedArray = [ 2, 4, 6 ]
In this example, the callback function is assigning new values at each index location of originalArray. Obviously, if you wanted to explicit update the original array like this, you probably wouldn't use .map(). However, its interesting to note that .map() does not complain about the callback function modifying the array on which it was called.
Conclusion
Things to consider when using .map():
- Write your callback function carefully because it can modify the original array.
- In your callback function, always create new objects for every object in the original array. Otherwise you will just be copying pointers to the original objects.
Song of the Week
- The Girl I Haven't Met - Kudasaibeats - Spotify/SoundCloud
Top comments (4)
I hardly disagree with the title of this article. For me, the biggest point here is:
Array.prototype.map()
does not mutate by itself the original array.It can change the original array if you write it in a way to do so. But the callback function is up to the developer, not up to specification and the specification says that the
.map()
doesn't mutate.Another good point of discussion is why the following code works the way it works:
By writing the arrow function with this shorthand it makes the same as
and if the arrow function is changed the like
then the
mappedArray
should be equal to theoriginalArray
before those changes. Just check the following code:Thank you so much for the feedback. I’m always trying to learn more, so I appreciate you taking the time to read and comment on my blog post.
You’re absolutely right that Array.prototype.map() will not mutate the original array by itself, which is why that’s the first thing I mentioned at the beginning of this post.
Now was my title for this post a little on the clickbaity side of things? Sure. But my point was that if we’re not thoughtful in the way we write the callback function, Array.prototype.map() can mutate the original array.
This post is intended for JavaScript beginners, and if as a beginner you believe that .map() will never mutate the original array, you’re going to run into this issue and have no idea why your original array is getting mutated.
I think we agree on the subject, we just disagree on the semantics.
You’re essentially saying that Array.prototype.map() doesn’t mutate the original array, developers who use Array.prototype.map() mutate the original array.
And you’re right, but at the end of the day Array.prototype.map() was executed and the original array got mutated.
Array.prototype.map() never executes itself. There is always a developer behind it.
Finally, thanks for pointing out how you can rewrite the arrow function in my last example to get a different behavior. That’s pretty cool and I hadn’t thought of that!
Your article was so helpful to me. Thanks for the clear explanation and examples. Helped me solve a re-rendering issue in React.
Thank you! This was driving me mad