DEV Community

Cover image for Javascript Polyfills: forEach, map, filter and reduce
S Shraddha
S Shraddha

Posted on

Javascript Polyfills: forEach, map, filter and reduce

The Javascript language has been evolving steadily over the years. New features on the language appear regularly. Some older browsers may not support these modern functionalities.

A polyfill is a piece of code that implements a feature on browsers that do not support the feature. Hence, the name - it fills the gap by adding missing implementations.

Writing polyfills is commonly asked during interviews. This article focuses on the same for the popular Javascript array methods - forEach, map, reduce and filter, while taking care of the edge cases.

For details on the usage and syntax of these array methods refer MDN | Array instance methods.

.forEach( )

The forEach method invokes the provided callback function for each element in the array.

Syntax

forEach(function callbackFn(element, index, array) { ... }, thisArg);

Enter fullscreen mode Exit fullscreen mode

Some important things to note about forEach:

  • callbackFn is called on each element of the array.
  • forEach returns undefined.
  • callbackFn must be called in the context of thisArg. If thisArg is not passed, callbackFn is invoked as a regular function.
  • If a callback function is not passed as the first argument, forEach throws a TypeError.
  • If forEach is invoked on null or undefined, it throws a TypeError

Let's begin with the first step:

if (!Array.prototype.myForEach) {
  Array.prototype.myForEach = 
function (callbackFn, thisArg) {
    for (let i = 0; i < this.length; i++) {
      callbackFn(this[i], i, this);
    }
  };
}

Enter fullscreen mode Exit fullscreen mode

We first check if the function is already available in the prototype chain of Array. this inside the function references the array on which forEach is called.

forEach also accepts an optional second argument - thisArg. If passed, the callback function must be invoked in the context of thisArg i.e. this inside callbackFn must be set to thisArg. This can be done using the call() method:

if (!Array.prototype.myForEach) {
  Array.prototype.myForEach = 
function (callbackFn, thisArg) {
    for (let i = 0; i < this.length; i++) {
      callbackFn.call(thisArg, this[i], i, this);
    }
  };
}

Enter fullscreen mode Exit fullscreen mode

Time to handle the error cases!

  • What if a callback function is not passed to forEach?
  • What if forEach is not invoked on an array?

In the above cases, an Error object must be thrown along with a descriptive message. Here, we will replicate the behavior shown by the original forEach method.

if (!Array.prototype.myForEach) {
  Array.prototype.myForEach = function (callbackFn, thisArg) {
    if (this == null || this === window)
      throw TypeError('Array.prototype.myForEach called on null or undefined');

    if (typeof callbackFn !== 'function')
      throw TypeError(`${callbackFn} is not a function`);

    for (let i = 0; i < this.length; i++) {
      callbackFn.call(thisArg, this[i], i, this);
    }
  };
}

Enter fullscreen mode Exit fullscreen mode

this == null || this === window - This condition is true if forEach is invoked as a standalone function (i.e not a method invocation). For example:

const myUnboundForEach = Array.prototype.myForEach;

myUnboundForEach();

Enter fullscreen mode Exit fullscreen mode

myUnboundForEach() is executed like a normal function expression. this inside the callback function will be the global object (window) in non-strict mode or undefined in the strict mode. Both these conditions are handled above. In both cases the TypeError is thrown.

And that's it! We have created our own implementation of the JS array method forEach and have also handled the error conditions.

The polyfill implementation for the rest of the methods are very similar and only differ in the core functionality of the method.

.map( )

The map method creates an array that contains values returned by the callback function invoked on each element in the calling array. Our function should now return the newly created array.

Syntax

map(function callbackFn(element, index, array) { ... }, thisArg);

Enter fullscreen mode Exit fullscreen mode

Polyfill

if (!Array.prototype.myMap) {
  Array.prototype.myMap = function (callback, thisArg) {
    if (this == null || this === window)
      throw TypeError('Array.prototype.myMap called on null or undefined');

    if (typeof callback !== 'function')
      throw TypeError(`${callback} is not a function`);

    const mappedArray = [];
    for (let i = 0; i < this.length; i++) {
      const mappedValue = callback.call(thisArg, this[i], i, this);
      mappedArray[i] = mappedValue;
    }
    return mappedArray;
  };
}

Enter fullscreen mode Exit fullscreen mode

.filter( )

The filter method creates an array that contains only those elements of the calling array that pass the test provided by the callback function.

Syntax

filter(function callbackFn(element, index, array) { ... }, thisArg);

Enter fullscreen mode Exit fullscreen mode

Polyfill

if (!Array.prototype.myFilter) {
  Array.prototype.myFilter = function (callback, thisArg) {
    if (this == null || this === window)
      throw TypeError(
        'Array.prototype.myFilter is called on null or undefined'
      );

    if (typeof callback !== 'function')
      throw TypeError(`${callback} is not a function`);

    const filtered = [];

    for (let i = 0; i < this.length; i++) {
      if (callback.call(thisArg, this[i], i, this)) filtered.push(this[i]);
    }

    return filtered;
  };
}

Enter fullscreen mode Exit fullscreen mode

.reduce( )

The reduce method works a little differently than the above methods. It accepts a reducer callback function that is called on each element of the array along with the returned value from the previous invocation. After calling the reducer across all array elements, the single, accumulated result is returned.

Syntax

reduce(function callbackFn(previousValue, currentValue, currentIndex, array) { ... }, initialValue);

Enter fullscreen mode Exit fullscreen mode

Some important things to note about reduce:

  1. The second argument to reduce is an optional initialValue, used to initialize previousValue.
  2. Value returned from callbackFn after traversing all elements of the array is ultimately returned from reduce.
  3. If initialValue is not provided, previousValue is initialized to the first element in the array, and reduce begins traversal from the second element in the array.
  4. If array is empty and initialValue is not provided, a TypeError is thrown.

Lets' begin with the main working of reduce:

if (!Array.prototype.myReduce) {
  Array.prototype.myReduce = function (callback, initialValue) {
    let previousValue = initialValue;
    let startIndex = 0;

    if (initialValue == null) {
      previousValue = this[0];
      startIndex = 1;
    }

    for (let index = startIndex; index < this.length; index++) {
      previousValue = callback(previousValue, this[index], index, this);
    }

    return previousValue;
  };
}

Enter fullscreen mode Exit fullscreen mode

This covers points 1, 2 and 3 above.

Time to handle the error cases:

  • What if initialValue is not provided and array is empty?
    In this case, previousValue will be assigned undefined. We can check for this and throw a TypeError with the appropriate error message.

  • Is a callback function passed?

  • Is reduce called on null/undefined?

All the above error cases are handled as follows:

if (!Array.prototype.myReduce) {
  Array.prototype.myReduce = function (callback, initialValue) {
    if (this == null || this === window)
      throw TypeError('Array.prototype.myReduce called on null or undefined');

    if (typeof callback !== 'function')
      throw TypeError(`${callback} is not a function`);

    let previousValue = initialValue;
    let startIndex = 0;

    if (initialValue == null) {
      previousValue = this[0];
      startIndex = 1;
    }

    if (previousValue == null)
      throw TypeError('Reduce of empty array with no initial value');

    for (let index = startIndex; index < this.length; index++) {
      previousValue = callback(previousValue, this[index], index, this);
    }

    return previousValue;
  };
}

Enter fullscreen mode Exit fullscreen mode

Wrapping Up

We saw the working of some commonly used Array methods along with their polyfill implementation, while handling the error cases.

Thank you for reading. Happy coding! 🙂

Top comments (2)

Collapse
 
shraddha319 profile image
S Shraddha

Hi! Yes, you're right.
I have been giving interviews from the last few months and writing polyfills for these methods was asked frequently asked. There are many tutorials online that teach people how to do it but many of them don't talk about how the error cases must be handled, which I feel is pretty important too and without it, the implementation would just be incomplete. So I decided to document it here.
I wasn't trying to propose anything new.

 
shraddha319 profile image
S Shraddha

Thank you for the advice! :)

I have edited the post to clarify my intention behind the article.