jsnotes

Topic 010: Polyfill in JS

Topic 010: Polyfill in JS


0. Promise.all()

Creating a polyfill for Promise.all() involves writing a function that behaves like the native Promise.all() method, which takes an iterable of promises (or values) and returns a single promise that resolves when all the input promises have resolved, or rejects if any of the input promises reject. Here’s how you can implement such a polyfill:

if (!Promise.all) {
  Promise.all = function (iterable) {
    // Return a new promise
    return new Promise(function (resolve, reject) {
      // Check if the input is an iterable
      if (!iterable || typeof iterable[Symbol.iterator] !== "function") {
        return reject(new TypeError("Argument is not iterable"));
      }

      var results = [];
      var completed = 0;
      var total = 0;
      var hasCalledReject = false;

      // Helper function to process each promise
      function processPromise(index, value) {
        Promise.resolve(value)
          .then(function (result) {
            results[index] = result;
            completed += 1;
            // If all promises are resolved, resolve the returned promise
            if (completed === total) {
              resolve(results);
            }
          })
          .catch(function (error) {
            if (!hasCalledReject) {
              hasCalledReject = true;
              reject(error);
            }
          });
      }

      // Iterate over the input
      var index = 0;
      for (let item of iterable) {
        processPromise(index, item);
        index += 1;
        total += 1;
      }

      // If the iterable was empty, resolve immediately
      if (total === 0) {
        resolve(results);
      }
    });
  };
}

Explanation:

  1. Check if Promise.all already exists: The polyfill checks if Promise.all is not defined to avoid overwriting the native implementation if it exists.

  2. Create a new promise: The polyfill function returns a new Promise.

  3. Check if the input is iterable: It verifies whether the provided argument is an iterable. If it’s not, it rejects the promise with a TypeError.

  4. Initialize variables:

    • results: An array to hold the results of the resolved promises.
    • completed: A counter to track the number of resolved promises.
    • total: The total number of promises in the iterable.
    • hasCalledReject: A flag to ensure the promise rejects only once if any input promise rejects.
  5. Process each promise: A helper function processPromise is defined to handle each promise in the iterable:

    • It converts each value to a promise using Promise.resolve.
    • On successful resolution, it stores the result in the results array and increments the completed counter.
    • If all promises resolve, it resolves the returned promise with the results array.
    • If any promise rejects, it rejects the returned promise with the encountered error.
  6. Iterate over the input: The function iterates over the provided iterable, processing each item with processPromise.

  7. Handle empty iterables: If the iterable is empty (i.e., total is 0), it resolves the returned promise immediately with an empty results array.


In JavaScript interviews, candidates might be asked to write polyfills for various methods and functionalities that are commonly used in modern JavaScript but may not be available in older environments. Here are some of the most common polyfills you might be asked to implement, along with their code examples:

1. Array.prototype.map

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

if (!Array.prototype.map) {
  Array.prototype.map = function (callback, thisArg) {
    if (this == null) {
      throw new TypeError("Array.prototype.map called on null or undefined");
    }
    if (typeof callback !== "function") {
      throw new TypeError(callback + " is not a function");
    }
    var O = Object(this);
    var len = O.length >>> 0;
    var T = thisArg || undefined;
    var A = new Array(len);
    for (var k = 0; k < len; k++) {
      if (k in O) {
        A[k] = callback.call(T, O[k], k, O);
      }
    }
    return A;
  };
}

2. Array.prototype.filter

The filter method creates a new array with all elements that pass the test implemented by the provided function.

if (!Array.prototype.filter) {
  Array.prototype.filter = function (callback, thisArg) {
    if (this == null) {
      throw new TypeError("Array.prototype.filter called on null or undefined");
    }
    if (typeof callback !== "function") {
      throw new TypeError(callback + " is not a function");
    }
    var O = Object(this);
    var len = O.length >>> 0;
    var T = thisArg || undefined;
    var A = [];
    for (var k = 0; k < len; k++) {
      if (k in O) {
        var kValue = O[k];
        if (callback.call(T, kValue, k, O)) {
          A.push(kValue);
        }
      }
    }
    return A;
  };
}

3. Array.prototype.reduce

The reduce method executes a reducer function (that you provide) on each element of the array, resulting in a single output value.

if (!Array.prototype.reduce) {
  Array.prototype.reduce = function (callback, initialValue) {
    if (this == null) {
      throw new TypeError("Array.prototype.reduce called on null or undefined");
    }
    if (typeof callback !== "function") {
      throw new TypeError(callback + " is not a function");
    }
    var O = Object(this);
    var len = O.length >>> 0;
    var k = 0;
    var accumulator;
    if (arguments.length >= 2) {
      accumulator = initialValue;
    } else {
      while (k < len && !(k in O)) {
        k++;
      }
      if (k >= len) {
        throw new TypeError("Reduce of empty array with no initial value");
      }
      accumulator = O[k++];
    }
    while (k < len) {
      if (k in O) {
        accumulator = callback(accumulator, O[k], k, O);
      }
      k++;
    }
    return accumulator;
  };
}

4. Function.prototype.bind

The bind method creates a new function that, when called, has its this keyword set to the provided value, with a given sequence of arguments preceding any provided when the new function is called.

if (!Function.prototype.bind) {
  Function.prototype.bind = function (oThis) {
    if (typeof this !== "function") {
      throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
    }
    var aArgs = Array.prototype.slice.call(arguments, 1);
    var fToBind = this;
    var fNOP = function () {};
    var fBound = function () {
      return fToBind.apply(
        this instanceof fNOP ? this : oThis,
        aArgs.concat(Array.prototype.slice.call(arguments))
      );
    };
    if (this.prototype) {
      fNOP.prototype = this.prototype;
    }
    fBound.prototype = new fNOP();
    return fBound;
  };
}

5. Object.create

The Object.create method creates a new object, using an existing object as the prototype of the newly created object.

if (typeof Object.create !== "function") {
  Object.create = function (prototype, propertiesObject) {
    if (prototype !== Object(prototype) && prototype !== null) {
      throw new TypeError("Object prototype may only be an Object or null");
    }
    function F() {}
    F.prototype = prototype;
    var obj = new F();
    if (propertiesObject !== undefined) {
      Object.defineProperties(obj, propertiesObject);
    }
    if (prototype === null) {
      obj.__proto__ = null;
    }
    return obj;
  };
}

6. Array.isArray

The isArray method determines whether the passed value is an Array.

if (!Array.isArray) {
  Array.isArray = function (arg) {
    return Object.prototype.toString.call(arg) === "[object Array]";
  };
}

7. String.prototype.trim

The trim method removes whitespace from both ends of a string.

if (!String.prototype.trim) {
  String.prototype.trim = function () {
    return this.replace(/^\s+|\s+$/g, "");
  };
}

8. Promise

Implementing a full polyfill for Promise can be quite extensive, but here is a basic polyfill for Promise:

(function (global) {
  if (typeof global.Promise !== "function") {
    global.Promise = function (executor) {
      if (typeof executor !== "function") {
        throw new TypeError("Promise resolver must be a function");
      }

      var state = "pending";
      var value;
      var handlers = [];

      function resolve(result) {
        if (state !== "pending") return;
        state = "fulfilled";
        value = result;
        handlers.forEach(handle);
      }

      function reject(error) {
        if (state !== "pending") return;
        state = "rejected";
        value = error;
        handlers.forEach(handle);
      }

      function handle(handler) {
        if (state === "fulfilled") {
          handler.onFulfilled(value);
        } else if (state === "rejected") {
          handler.onRejected(value);
        } else {
          handlers.push(handler);
        }
      }

      this.then = function (onFulfilled, onRejected) {
        return new global.Promise(function (resolve, reject) {
          handle({
            onFulfilled: function (result) {
              if (typeof onFulfilled === "function") {
                try {
                  resolve(onFulfilled(result));
                } catch (e) {
                  reject(e);
                }
              } else {
                resolve(result);
              }
            },
            onRejected: function (error) {
              if (typeof onRejected === "function") {
                try {
                  resolve(onRejected(error));
                } catch (e) {
                  reject(e);
                }
              } else {
                reject(error);
              }
            },
          });
        });
      };

      executor(resolve, reject);
    };
  }
})(this);

9. Array.forEach()

const arr = [1, 3, 4, 5, 5, 6];

const logger = (item, index) => {
  console.log(item, index);
};

Array.prototype.forEach = function (callBack) {
  for (let i = 0; i < this.length; i++) {
    callBack(this[i], i);
  }
};

arr.forEach(logger);

10. Array.flat()

const arr = [1, 2, 3, 4, 5, [6, 4, 3, 5, [1, 2, 3]]];

function myFlat(arr, depth = 1, output = []) {
  if (depth <= 0) {
    output.push(arr);
    return output;
  } else {
    for (const item of arr) {
      if (Array.isArray(item)) {
        myFlat(item, depth - 1, output);
      } else output.push(item);
    }
  }
  return output;
}
console.log(myFlat(arr, 10));