Learning JavaScript deeply — Understanding map

Fredrik Strand Oseberg
Frontend Weekly
Published in
8 min readJun 7, 2017

So far in this series we’ve deconstructed the array.prototypes forEach and filter, and built our own versions of them. So let me elaborate in this article on why we’re doing this and why you should care.

Programming is hard. One of the reasons it’s hard is because it’s so abstract. You can’t point to a function call and explain what it is the same way you could point to a cat. There is no physical embodiment of your code, and there are no emotions tied to programming to aid your comprehension.

All you have is your education, your previous attempts of thinking in the abstract, to guide you on this path of learning.

And yes, comprehension is the goal. If you accept the premise that learning is comprehension, then you’ll quickly understand why we need to do this.

It’s not enough to read the documentation and learn what the function does, in order to comprehend we need to understand how it does what it does.

The goal of this article will be to go through the map function that exist on array.prototype.

If you haven’t yet read the articles on forEach and filter, I suggest you do so to gain a basic understanding, and by the time you finish this one, you will undoubtedly start to see a pattern emerge in how these functions work.

MDN as a starting point

As usual, we’ll visit MDN to get a basic understanding of what the function does.

The map() method creates a new array with the results of calling a provided function on every element in the calling array.

Syntax:

var new_array = arr.map(function callback(currentValue, index, array) {
// Return element for new_array
}[, thisArg])

Example:

var numbers = [1, 5, 10, 15];
var doubles = numbers.map(function(element) {
return element * 2;
});
// doubles is now [2, 10, 20, 30]
// numbers is still [1, 5, 10, 15]

Ok so map takes a callback function, which takes additional parameters, and an optional this object.

In the example, the element is being doubled in value by the statement element * 2 inside of the callback function and returns a new array, leaving the original array untouched.

We can see that each element from the original array exists in the new doubles array with their values doubled.

So let’s take a look at the callback function. According to the documents, the callback function takes three arguments (see syntax block above):

(1) currentValue, the current element being run (we’ll see what this means soon)

(2) index, the index of the current element

(3) the original array that the callback function operates on.

Building our own map

Now that we have a pretty good idea of what map does. Let’s try to build our own. We’ll start with an empty function definition and build step by step:

function map(array, callback, thisObject) {}

Ok, so our array takes three parameters:

(1) the array to operate on

(2) a callback function

(3) an optional thisObject

Since we’re not adding the map to the array prototype we need to add the array to operate on as the first argument. If you’d like to you could create a new function on the prototype by doing:

Array.prototype.myMap = function(callback, thisObject) {}

This would mitigate the need for the array as the first argument.

Moving on.

Let’s break down what this function should do:

  1. It should perform some action on each item using a callback function and pass in each element of the array
  2. It should have index and original array as second and third parameters in the callback
  3. It should return a new array with the new items
  4. It should not mutate the original array
  5. It should have a configurable this object

Let’s tackle this one step at the time.

It should perform some action on each item using a callback function and pass in each element of the array

In order to perform an action on an item inside an array we need to use a loop in order to get to each item. So let’s add a for loop into our map function:

function map(array, callback, thisObject) {
for (var i = 0; i < array.length; i++) {
callback();
}
}

Ok. Now we have a map function that iterates through the length of the array specified and calls the callback one time for each item. Let’s test it out:

map([1, 2, 3], function (element) {
console.log(element); // undefined
});

What the what? This example is undefined. Well of course! We’re not passing anything into the callback function in our map function. So the element argument is set to undefined. Let’s fix that.

function map(array, callback, thisObject) {
for (var i = 0; i < array.length; i++) {
callback(array[i]);
}
}

Now we’re passing in each element of the array in our callback function. What this means is that when map is invoked, we are running callback and passing in array[i] as an argument which in our example translates to:

map([1, 2, 3], function (element) {
console.log(element); // 1, 2 3
});

The first time the for loop is run array[i] is equal to 1, the second time array[i] is equal to 2 and the third time array[i] is equal to 3. So 1, 2, 3 is what is passed as arguments to our callback function and that is what is logged to the console.

Now we have completed the first requirement of our function.

It should have index and original array as second and third parameters in the callback

The callback is still lacking the ability to access the index of the element being processed and the original array. Let’s get to work on that:

function map(array, callback, thisObject) {
for (var i = 0; i < array.length; i++) {
callback(array[i], i, array);
}
}

As you can see from the above example. It’s relatively simple to do. We’re passing i, which represents the index of the array in as the second argument to the callback and array as the third. Let’s see it in an example:

map([1, 2, 3], function (element, index, originalArray) {
console.log(index); // 0, 1, 2
console.log(originalArray) // [1, 2, 3]
});

Now we can access these variables in our callback function. Index points to the value of i in the for loop of map, and originalArray points to the array passed in as the first argument to the map function.

Thats it for this requirement! On we go.

It should return a new array with the new items

Ok so how do we return a new array with the new items? We need some kind of variable to hold the items inside of the map function right? So let’s do that.

function map(array, callback, thisObject) {
var mapped = [];

for (var i = 0; i < array.length; i++) {
if (i in array) {
mapped[i] = callback(array[i], i, array);
}
}
return mapped;
}

Ok so what we’ve done here is to add an if statement that basically covers some weird corner case we’re not going to spend time on explaining, but the important part is the mapped[i] = callback(array[i], i, array).

What we are saying here is that mapped is equal to the return value of callback, which is executed array.length times.

What this means is you will end up with exactly the same items in the mapped array as in the original array. Ok so now we’ve covered the part of adding the items from the callback to a new array and returning the value. Let’s see an example before we move on:

map([1, 2, 3], function (element, index, originalArray) {
return `The index of ${element} is ${index}`;
});
// ["The index of 1 is 0", "The index of 2 is 1", "The index of 3 is 2"]

The return statement here throws many people of, because we’ve learned that return ends a function, and indeed it does.

But remember, we know now that the callback is called array.length times, so each time there is a new callback with new parameters given to the callback from the map function.

If you’re brain is hurting now, thats ok. We’re almost done.

It should not mutate the original array

This requirement has become a side effect of what we have done so far. By pushing all of our changes from the callback function into a new array and returning that array from inside the map function, we ensure that we do not mutate the original array.

So let’s do the last feature of our function

It should have a configurable this object

The last feature of our function is that we want to be able to specify a thisObject that the function should be able to utilise with the this keyword. So for example we want to be able to:

map([1, 2, 3], function (element, index, originalArray) {
console.log(this.name) // 'this object to be used in function'
}, { name: 'this object to be used in function'});

This looks complicated. So let’s break it down. Remember our function takes three parameters, let’s illustrate with an empty callback function:

// function declarationfunction map(array, callback, thisObject) { // original parameters
// code that makes map tick
}
// specify this object
var thisObj = {
name: 'this object to be used in function'
}
map([1, 2, 3], function() {}, thisObject)

In the block above we first have the function declaration to remind us of the parameters. Then we have a custom this object that we specify and save in the variable thisObj.

On the last line we invoke map with an empty callback function to make it crystal clear what the parameters are in this function.

Let’s build the functionality. In order to bind the this object of a function we can use the bind method that exist on the function prototype (if you’ve read the previous articles, this is review for you now).

Bind will return a function that has it’s this object set to whatever you specify as an argument. Let’s see how we can use this in our map function:

function map(array, callback, thisObject) {
var mapped = [];
var mapCallback = callback;
if (thisObject) {
mapCallback = callback.bind(thisObject);
}

for (var i = 0; i < array.length; i++) {
if (i in array) {
mapped[i] = mapCallback(array[i], i, array);
}
}
return mapped;
}

Ok so let’s go through the function and see whats new for the last time.

First, we added a new variable called mapCallback and set it equal to callback (which is passed in when function is invoked). Then we check if the thisObject is specified as an argument.

If it is, then we set mapCallback equal to callback.bind(thisObject) which will return a new function that has it’s this object set to the thisObject argument specified at when map is invoked.

Finally, we replace the function call inside the for loop with our new mapCallback variable. The result?

map([1, 2, 3], function (element, index, originalArray) {
console.log(this.name) // 'I am the this object'
}, { name: 'I am the this object'});

The example now correctly logs ‘I am the this object’ to the console. Hooray!

If you made it this far, great work! You’re now 100% comfortable with what goes on with map under the hood, and you cleared up a lot of smoke and mirrors that hides what really happens in JS. Awesome.

Feel free to follow me on Medium and Twitter at @foseberg, if you’d like to follow my articles and insights on learning JavaScript deeply, or sign up for my newsletter below.

--

--

Fredrik Strand Oseberg
Frontend Weekly

Entrepreneur, currently on a journey of learning how to code.