Functional Programming JavaScript Map() Function Demystified

Functional programming in JavaScript relies heavily on concepts that are utilized in the built-in map, reduce and filter methods.

Therefore, since these methods are used frequently in modern day JavaScript programming, understanding how they work is of utmost importance, especially if you are concerned about staying relevant in this field.

I believe that if you are fairly new to functional programming, you can learn a lot by simply studying these built in methods.

Why? Because these methods employ the core concepts of functional programming.

In this post, we will specifically be examining and scrutinizing the JavaScript map() method.

By the end of this tutorial, my hope is that you will be able to write your own implementation of the JavaScript map() method. Naturally, you will become much more accustomed to functional programming and also hopefully, learn a thing or two about the JavaScript language as well.

I am going to assume that you know a little about the basics of functional programming.

If not, I recommend reading an introduction to functional programming in JS before proceeding. Because we will be writing some code based off fundamental functional programming concepts.

In this tutorial, I am going to be testing your knowledge at certain points. Of course, I will provide as much background information as needed before throwing you into the pool.

Don’t worry, by the end of this tutorial, you will feel pretty confident about functional programming.

When you first hear the phrase “JavaScript functional programming methods”, what are the methods that first come to mind?

If you are like me, it would probably be along the lines of

  • map
  • filter
  • reduce

Did you notice how I omitted forEach? I will leave that up to your imagination, but let me give you two words: pure functions. Hope that rings a bell as to why I have not included it in the list above.

However, we will examine and write out a basic implementation for the forEach() function.

Why?

Because writing out the implementation of forEach() will help us understand the aforementioned functional programming methods.

forEach() Implementation in JavaScript

Before starting, I want to mention one important point: forEach() is not a pure function. When using forEach(), you are most likely directly manipulating an external mutable object.

That is the whole reason why you are iterating through the object in the first place right?

You might be asking then,

Why start off by examining forEach?

Even though forEach() is not a pure function, it trains us in

  • getting used to using higher order functions, which is essential when applying functional programming principles in JavaScript.
  • iterating through a collection and applying an operation to the result set.

According to Professional JavaScript for Web Developers on page 175 (2012 edition), enumerable objects are objects that have properties that will be returned in a for-in loop. I.e.

for ( var key in obj)

forEach() iterates over the passed in enumerable object and applies the passed in function. The passed in function accepts three arguments. The first is the value, second is the key. Lastly, the third argument is the actual object.

array.forEach(function(value, key, list) {
    // Insert logic here.
});

Note that the forEach() implementation in this post will work on both objects and arrays. In the traditional ECMAscript 5 Array.prototype.forEach method will only work on arrays.

forEach() code snippet

I am intentionally throwing you first into the deep end. Don’t worry, I am not going to leave you to fend for yourself and potentially drown in the vicious ocean called functional programming.

If you don’t understand the code below, I am giving you a glimpse of what you can potentially learn in this tutorial.

I am going to do everything in my power to make sure you are able to understand the code below and even write it out on your own without having to refer to any other material.

var isObject = function isObject(obj) {
      return Object.prototype.toString.call(obj) === "[object Object]";
};

var isArray = Array.isArray || function isArray(obj) {
    return Object.prototype.toString.call(obj) === "[object Array]";
};

/**
 * @param {Object|Array} obj can either be an object or array. Otherwise, throw error
 * @param {Function} fn the callback function passed.
 * */
function forEach(obj, fn) {
    var key;
    if (isArray(obj)) {
        for (key = 0; key < obj.length; key++) {
            fn(obj[key], key, obj);
        }
    } else if (isObject(obj)) {
        for (key in obj) {
            if (obj.hasOwnProperty(key)) {
                fn(obj[key], key, obj);
            }
        }
    } else {
        throw new Error(obj + " is not of type Object or Array");
    }
}

If you don’t understand, take a deep breath and be excited! By the end of this tutorial, you should understand how the code above works.

Pure Functions Revisited

Okay, we are going to revisit pure functions briefly. In my introduction to functional programming, we established that pure functions avoid

  • Shared states
  • Mutable data
  • Side-effects

Let us examine the first function in the example above: isObject().

The basic gist of the three points above is this.

The function should not manipulate any objects declared outside of the function, as well as the item that was passed in.

And also, functions should only be responsible for a single task. Nothing more, nothing less. For more information, I recommend reading an article on pure functions.

Personally, I recommend taking extreme care when naming variables and functions, regardless of what programming language you write in. I will admit, sometimes I get lazy with thinking of variable names and I have to rebuke myself.

The name isObject naturally suggests that the method checks whether or not the passed in object is of type object. One problem I have with this name however, is that a lot of developers, surprisingly have very different interpretations of exactly what an “object” is.

For the purposes of this tutorial, the object here is a JavaScript object. Like the example below.

var obj = {};

Examining isObject() Method

Okay, lets take a look at the code first.

var isObject = function isObject(obj) {
    return Object.prototype.toString.call(obj) === "[object Object]";
};

Like the isArray() method, isObject() checks whether the passed in item is a JavaScript object and returns true if it is. Otherwise, it returns false.

In the following line

return Object.prototype.toString.call(obj) === "[object Object]";

Object.prototype.toString.call(obj) is essentially calling the Object’s built-in toString() method with this set to obj. For more information on Object.prototype.toString, I recommend

reading this stackoverflow thread.

Maybe looking at the table below will give you a better idea of what is returned when called on typed variables. Here is a list based off the ES5 Spec.

Object.prototype.toString.call( value ) where value equals Result
Undefined [object Undefined]
Null [object Null]
Boolean [object Boolean]
Number [object Number]
String [object String]
Object (native and not callable) E.g. var obj = {}; [object Object]
Object (native or host and
callable) AKA – Functions. E.g. var fn = function() {};
[object Function]

If you don’t believe me, try typing these out yourself and log the results onto a console. I created the table above by manually double checking all the results returned with various data types.

Before moving onto writing and examining the JavaScript map() implementation, we need to examine one more topic: reference and value types in JavaScript.

Examining Reference and Value Types

Before we proceed, you need to fully understand how reference and value types work in JavaScript.

I scratched the surface in the previous section, but here, we will fully uncover and expose the root cause of some of the side-effect. These side-effects are more often than not, caused by a misunderstanding of reference and value types. Naturally, we will also cover how to prevent these side-effects as well.

Simply put, peopleArrOriginal and peopleArrCopy contain objects that point to the same location in memory. Objects in JavaScript are reference types, but primitive values such as strings and numbers are value types.

Below is a list of items that are value types (thank you Mozilla Developer Network for the information on data types). Any data that is not a primitive, is a reference type in JavaScript.

Six data types that are primitive data types:

  • Boolean
  • Null
  • Undefined
  • Number
  • String
  • Symbol (new in ECMAScript 6)

Value Type Example

Suppose we have the following code.

var nums = 1;
var test = nums;

Note that since nums and test store a number type, they are value types.

In another words, in the following line

var test = nums;

what is happening behind the scenes is that a copy of the value stored in nums is being assigned to test.

call by value memory diagram

To build upon what we have established and also to solidify our existing knowledge, let us examine the following scenario. Suppose we were to add a couple of lines to the code snippet above.

test = 10;
console.log(nums);   // Guess what is logged
console.log(test);

Care to take a guess at what the answer is?

Answer

nums is still 1. Remember, nums and test are NOT pointing at the same location in memory. In JavaScript, when a value type is passed into a function or assigned to a new variable, a copy of that variable is made and inserted.

Reference Type Example

Reference values are a pointer to a location in memory where the object is stored (credits to stackoverflow.com for this answer). The topic of memory will, in it of itself, take up an entire post. Even an entire post may not suffice, therefore, I won’t be going in depth here.

I mentioned memory, because we will need to have memory in mind as we examine how reference types work under the hood.

Let us revisit the following code snippet.

var person1 = {
    name: "Jay",
    computers: ["Macbook Air", "Toshiba", "Acer desktop"]
};

var person2 = {
    name: "Bob",
    computers: ["Acer desktop", "Lenovo Thinkpad", "Microsoft Surfacebook Pro"]
};

In the example above, the variables person1 and person2 are pointing to a location in memory where the values are stored. Below is a visual diagram to convey my message.

reference values memory diagram example 1

Lets say now that we adding the following code snippet to the example. Remember that in JavaScript, objects are reference values. Take a guess at what the following code will output.

var person3 = person2;
person3.name = "Mike";

console.log(person2.name);  // ??

Since we have rambled on about reference types so much, the answer should come fairly quickly.

Even though we manipulated person3's  name property, because both person3 and person2 are pointing at the same location in memory, "Mike" will be outputted onto the console.

Below is what the memory looks like visually.

reference values memory diagram example two

JavaScript Map() Implementation

Before starting, lets first define what map does.

Creates a new array with the results of calling a provided function on every element in this array.

Source: Mozilla Developer Network

Like forEach(), the function passed into map() has three arguments.

  1. The actual value.
  2. The key (in arrays, this would be the index).
  3. The list object (or array).

Sometimes, reading the code and seeing the result is the best explanation. Try running the code snippet below yourself. In order to improve your skills as a programmer, you need to code. Therefore, I recommend everybody who reads this post to actually type out the code yourself to solidify what you learn.

var numMap = nums.map(function(value, index, numsArray) {
    console.log(value);     // 1, 2, 3
    console.log(index);     // 0, 1, 2
    console.log(numsArray); // [1, 2, 3] --> The array stored in the variable nums;
    
    // multiple value by 2
    return value * 2;        
});
console.log(numMap);  // [2, 4, 6]

Not that difficult right?

Code

Below is a somewhat simplified version of the JavaScript map function. I have gotten rid of the unnecessary complexities, leaving only what is most important. For the actual poly-fill, you can check out the map poly-fill at Mozilla Developer Network. Well, here is the simple version for us to break down and scrutinize together.

if (!Array.prototype.ourMap) {
    Array.prototype.ourMap = function ourMap(callback) {
        // this keyword refers to the object we call "ourMap()"on
        var result = [];
        var originalArray = this;
        var i = 0;
        for (; i < originalArray.length; i++) {
            result.push(callback(originalArray[i], i, originalArray));
        }
        return result;
    };    
}

var mappedNums = nums.ourMap(function(value, index, numsArray) {
    // multiple value by 2
    return value * 2;
});

console.log(mappedNums); // returns [2,4,6]

The following check if (!Array.prototype.ourMap) is a guard to ensure that we are not overwriting an existing implementation of ourMap().

If you ever have to write a poly-fill, this ensures that you do not end up breaking other people’s code by unintentionally overwriting their implementation with your own.

Where the magic occurs is in the following logic.

for (; i < originalArray.length; i++) {
    var manipulatedArrayItem = callback(originalArray[i], i, originalArray);
    result.push(manipulatedArrayItem);
}

Assuming you are comfortable with for loops, the question that we need to ask ourselves is the following

What are we pushing into the array?

In case you did not notice, I renamed the returned item of the callback function to manipulatedArrayItem. It is because I want to emphasize that the map function is used to return a fresh new array that contains the manipulated results of the original array.

Below is a diagram that shows the process by how the array var nums = [1, 2, 3]is translated and stored in mappedNums as [2, 4, 6].

functional programming JavaScript map diagram

In the first iteration, we are operating with the following parameters.

value = 1, index = 0, numsArray = [1,2,3];

 

Second iteration

value = 2, index = 1, numsArray = [1,2,3];

 

Third iteration.

value = 3, index = 2, numsArray = [1,2,3];

 

Here is a table of the parameter values passed into the callback function.

Iteration Value Index List
1 1 0 [1, 2, 3]
2 2 1 [1, 2, 3]
3 3 2 [1, 2, 3]

Do you see what is happening? On each iteration, we are moving one index forward in the array that we are operating on.

The callback function is merely an interface that enables us to interact with the items inside of the array we are working with.

Potential Dangers when using map()

Now that we understand how the JavaScript map() function works behind the scenes, let us discuss the possible dangers. It was briefly alluded to in the previous section.

Why don’t you think about it for a moment before coming back to this post?

Find out about potential dangers when using map()

Side effects.

You thought just because we were using map and not forEach() that you could avoid side effects?

If you did, no need to be ashamed. Unfortunately, a lot of people, especially those who are beginning to explore functional programming in JS are of the same mind.

In the upcoming section, I am going to blatantly expose some of the ways how we can still produce nasty side effects when using map to create new arrays.

Example One

Take a look at the following code.

var nums = [1,2,3];

var numMap = nums.map(function(value, index, numsArray) {
    numsArray[index] = 10;
    // multiple value by 2
    return value * 2;
});

console.log("Check for yourself what is logged onto the console");
console.log(nums);
console.log(numMap);

What is the first thing that sticks out to you?

Hint: we are violating a key fundamental principle behind pure functions.

Yes, numsArray[index] = 10; looks a bit fishy right? That line there is manipulating the original array (which in this case is nums).  Therefore, on the console, the following will be logged

num --> (3) [10, 10, 10]

 

numMap --> (3) [2, 4, 6]

 

In the example above, by assigning a new value to an element in the array passed as the third parameter, we have updated the original array, thus creating a side-effect.

Therefore, even when using map(), we need to be extremely careful if we ever make a decision of utilizing the third argument. An extreme purist would argue that the third argument should never be used in any circumstance.

If you do decide to use the third argument for something and it cannot be avoided, I highly recommend leaving a comment to remind the other developers reading the code, or yourself a few month down the track, exactly why you decided to use it and what its purpose is.

In conclusion, if possible, I highly recommend not using the third argument when using map() or any of the methods that passes the original array as one the its arguments.

Example Two – Dealing with Reference Types

I guess I sort of gave it away with the title right? By now, you should be familiar with reference and value types. If you are still not familiar with the difference between call by value vs reference, I highly recommend reading about and understanding it before proceeding. Otherwise, you are going to be in a world of confusion when you read the code below.

var person1 = {
    name: "Jay",
    computers: ["Macbook Air", "Toshiba", "Acer desktop"]
};

var person2 = {
    name: "Bob",
    computers: ["Acer desktop", "Lenovo Thinkpad", "Microsoft Surfacebook Pro"]
};

var peopleArrOriginal = [person1, person2];

var peopleArrCopy = people.map(function(person, index) {
    var computers = person.computers;
    person.name = person.name + " mapped!";
    computers.push("LG Gram");
    return person;
});

console.log(peopleArrOriginal);
console.log(peopleArrCopy);

In the code snippet, we created two objects, assigned to variables named person1 and person2 respectively. We create a new array that holds the two objects.

Take a look at the function passed into map().

We are doing two things here.

  1. Manipulating the name property.
  2. Adding an extra computer to the list of computers.

Do you think that the items inside peopleArrOriginal and peopleArrCopy are identical? Think about this for a moment. When you are ready, feel free to check the answer below.

Click for Answer

Although the memory location/reference of the resulting array (peopleArrOriginal and peopleArrCopy respectively) are different, the memory location objects that the contents of the array are pointing to are identical. Why? because the items inside of the array are reference values, pointing at the same location in memory.

How to remove side effects from map()

One easy way to remove side effects is to check whether an object is a reference type or not. Afterwards, if it is a reference type, create a deep copy of the object and do so recursively until we create deep copies of all reference types and return the result.

You can probably tell, judging by the amount of steps required that this solution takes up a lot of resources.

Depending on the size of the data set you are working with, you might want to adopt a different solution.

Remember, as software engineers and/or programmers, we need to approach each problem differently, depending on user requirements and other available information at hand.

Homework

After reading a post, the best way to retain the knowledge acquired is to apply it right?

Don’t even think about getting away without a proper workout!

As of now, I haven’t coded all the solutions, due to a hectic work schedule, because I am currently in a busy season at work.

I will work on writing out the solutions as soon as possible. In the meanwhile, check out the basic implementations of forEach, map and filter on GitHub.

As always, please send in your feedback and let me know areas that can be improved. I want readers to get the most out of these posts, as well as the exercises.

Like any of my other posts, I will continue to update the content, evolve it, add more exercises and so on.

Below is your homework. Enjoy!

Warm up Exercises

  1. Create and write your own implementation of forEach() and map().
  2. In the people example above (with var person1 and person2), write an implementation that will ensure that the result set produced all point at different memory location. In another words, remove the side effects from the example above.

Intermediate Exercises

  1. Write a function that merges two arrays together. Note: make sure that the resulting array has no side effects.
  2. Update the previous function to merge N arrays passed together. Make sure to handle errors such as users passing in a value that is not an array.
  3. Write a function that merges two objects together. No side effects okay?
  4. Write a function that flattens an array and returns a new array. Create a regular flatten that behaves like the regular map (one that flattens the first level).
    • E.g. passing in [1, 2,[3, 4, [5,6] ] ] should return [1,2,3,4,[5,6]];
  5. Afterwards, create one that deep flattens.
    • E.g. passing in [1, 2,[3, 4, [5,6] ] ] should return [1,2,3,4,5,6];

About the Author Jay

I am a programmer currently living in Seoul, South Korea. I created this blog as an outlet to express what I know / have been learning in text form for retaining knowledge and also to hopefully help the wider community. I am passionate about data structures and algorithms. The back-end and databases is where my heart is at.

follow me on:
44 Shares