Skip to main content

Command Palette

Search for a command to run...

PolyFills in JavaScript

Updated
•9 min read•View as Markdown
PolyFills in JavaScript
O

React Front End Developer.

Languages like JavaScript keep on updating from time to time. As the language updates so do the engines compiling the code. Often we require a feature in the newer versions of the language to support on older engines.

Polyfills are a way to provide backwards compatibility for newer features by implementing the new features with supported code. Polyfills should not be confused with shims which are used to merely correct existing features.

While shims are used for covering up old sins, polyfills are used for bringing future enhancements back in time. - StackOverflow

Implementing a Polyfill

With the definition and usage out of our way, how do we actually go about implementing a polyfill for some function? Let us take an example here. If you have read my previous blog on Functional Array Methods, the array.map() was a new addition in ES5. It did not exist (atleast in the way we know and use it today) in the earlier versions. So, let us try to implement one without using anything from ES5 and above.

Note - We would not be using let and const as they were introduced in ES6. They can still be used for demonstrating some examples.

What do we know about array.map() ?

  • It works on an array.

  • It iterates over every element of the array.

  • It runs a callback on each element.

  • Callback takes 3 parameters (current_value, index, original_array).

  • The callback’s return value is collected into a new array.

  • It returns a new array without mutating the original one.

For now the above information is good enough to create a barebones polyfill for map. We will be creating our own method on Array prototype.

Step 1 : Create a barebones map on Array.prototype

  • We start with a simple version to see the structure.
Array.prototype.map = function() {
  // Empty for now
};

Step 2 : Iterate over the array.

  • The this keyword refers to the array on which map is called.
Array.prototype.map = function() {
  for (var index = 0; index < this.length; index++) {
    // We'll process each element here later
  }
};

Step 3 : Add a callback parameter

  • The callback receives three arguments: currentValue, index, and array.

  • This is invoked for each iteration.

Array.prototype.map = function(callback) {
  for (var index = 0; index < this.length; index++) {
    callback(this[index], index, this);
  }
};

Step 4: Collect results in a new array

  • We create a result array and push the callback return values.
Array.prototype.map = function(callback) {
  var resultArray = [];
  for (var index = 0; index < this.length; index++) {
    resultArray.push(callback(this[index], index, this));
  }
};

Step 5 : Return the result array

Array.prototype.map = function(callback) {
  var resultArray = [];
  for (var index = 0; index < this.length; index++) {
    resultArray.push(callback(this[index], index, this));
  }

  return resultArray;
};

Final version :

var numbers = [11, 32, 65, 93, 77]

Array.prototype.map = function(callback) {
  var resultArray = [];
  for (var index = 0; index < this.length; index++) {
    resultArray.push(callback(this[index], index, this));
  }

  return resultArray;
};

var numbers = [11, 32, 65, 93, 77]

function addOne(currentValue) {
  return currentValue + 1
}

console.log(numbers.map(addOne))
// [12, 33, 66, 94, 78]

This works for most cases, but it’s still a simplified version. The real .map() has extra checks and supports more edge cases.

Your Turn

Try writing a polyfill for filter and reduce. Do tell your answers in the comments!

(Hint: for filter, only two lines change!)

Going the extra mile…

If you have been working with Javascript and the array methods for quite some time, what more things are missing in the above implementation?
Think about it…
Better yet, try to read the specs.

  • The above implementation is clearly lacking the 2nd argument thisArg

  • What if someone forgot to add a callback?

  • What if someone provided a sparse array? (e.g. [1,,3].map(…))

  • What is someone misuses this on null or undefined values?

Let’s make our map closer to the real thing by handling edge cases and a few spec requirements.

Step 6: Validate this

  • To avoid errors, ensure map is not called on null or undefined.
Array.prototype.map = function(callback) {
  if (this == null) throw new TypeError("Array.prototype.map called on null or undefined");

  var resultArray = [];
  for (var index = 0; index < this.length; index++) {
    resultArray.push(callback(this[index], index, this));
  }
  return resultArray;
};

console.log([1,2,3].map(x => x + 1)); // [2,3,4]
// [1,2,3].map.call(null, x => x); // Uncommenting this throws TypeError

Step 7: Validate the callback function

  • Check that a function is passed; otherwise throw an error.
Array.prototype.map = function(callback) {
  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 resultArray = [];
  for (var index = 0; index < this.length; index++) {
    resultArray.push(callback(this[index], index, this));
  }

  return resultArray;
};

console.log([1,2,3].map(x => x + 1)); // [2,3,4]
// numbers.map("not a function"); // Uncommenting this throws TypeError

Step 8: Convert this to Object & pre-allocate result array

  • In this step, we make our polyfill more robust by handling array-like objects and preparing the output array:

    • Object(this) ensures that our map works not only on arrays, but also on array-like objects (like arguments).

    • length >>> 0 converts the length to a non-negative integer, safeguarding against invalid lengths.

    • new Array(length) pre-allocates the result array, preserving the original array’s size and structure, which is important for sparse arrays.

    • This also ensures that elements added or removed after the iteration starts are not processed, mimicking native map behavior on mutated arrays.

  • Now we make sure that our map also works on array-like objects and primitive values, not just arrays

Array.prototype.map = function(callback) {
  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 arrayLike = Object(this);           // Convert 'this' to object which includes primitive as well
  var length = arrayLike.length >>> 0;    // Ensure valid non-negative integer
  var resultArray = new Array(length);    // Pre-allocate result array

  for (var index = 0; index < length; index++) {
    resultArray[index] = callback(arrayLike[index], index, arrayLike);
  }

  return resultArray;
};

// Example 1: Array-like object
var arrayLike = {0: 10, 1: 20, 2: 30, length: 3};
var doubled = Array.prototype.map.call(arrayLike, x => x * 2);
console.log(doubled); // [20, 40, 60]

// Example 2: Sparse array
var sparseArray = [1,,3]; // index 1 is missing
var incremented = sparseArray.map(x => x + 1);
console.log(incremented); // [2, undefined, 4]

// Example 3: Mutated array
var numbers = [1,2,3];
numbers.map((x, i, arr) => {
  if (i === 0) arr.push(4); // Add element after iteration starts
  return x * 2;
});
// Only the original elements [1,2,3] are processed

// Example 4: String primitive
var letters = "abc";
var uppercased = Array.prototype.map.call(letters, x => x.toUpperCase());
console.log(uppercased); // ['A', 'B', 'C']

// Example 5: Number primitive (technically array-like has no length, returns empty array)
var num = 123;
console.log(Array.prototype.map.call(num, x => x * 2)); // []

By converting this to an object, Step 8 ensures that our .map works not just on arrays, but also on array-like objects and primitives, while still safely handling empty or invalid lengths.

Step 9: Skip sparse array holes

Now, if we don’t account for sparse arrays, the resulting array may contain undefined values as you can see in the examples above.
Native .map() does not invoke the callback on missing elements in sparse arrays.

Array.prototype.map = function(callback) {
  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 arrayLike = Object(this);
  var length = arrayLike.length >>> 0;
  var resultArray = new Array(length);

  for (var index = 0; index < length; index++) {
    // Skip holes in sparse arrays
    if (index in arrayLike) {
      resultArray[index] = callback(arrayLike[index], index, arrayLike);
    }
  }

  return resultArray;
};

// Example: Sparse array
var sparseArray = [1,,3]; // index 1 is missing
console.log(sparseArray.map(x => x * 2)); // [2, <1 empty item>, 6]

Step 10: Support optional thisArg

In native Array.prototype.map, the callback can have a custom this context. By default, this inside the callback is undefined in strict mode, or the global object in non-strict mode. Sometimes, you might want the callback to use your own object as this. That’s where thisArg comes in.

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 arrayLike = Object(this);
  var length = arrayLike.length >>> 0;
  var resultArray = new Array(length);

  for (var index = 0; index < length; index++) {
    if (index in arrayLike) {
      // Set 'this' inside the callback to thisArg if provided
      resultArray[index] = callback.call(thisArg, arrayLike[index], index, arrayLike);
    }
  }

  return resultArray;
};

// Example: thisArg usage
var context = { factor: 3 };
console.log([1,2,3].map(function(x) { return x * this.factor; }, context)); // [3,6,9]

Step 11: Only define the polyfill if map is not already defined [FINAL]

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 arrayLike = Object(this);
    var length = arrayLike.length >>> 0;
    var resultArray = new Array(length);

    for (var index = 0; index < length; index++) {
      if (index in arrayLike) {
        resultArray[index] = callback.call(thisArg, arrayLike[index], index, arrayLike);
      }
    }

    return resultArray;
  };
}


console.log([1,2,3].map(x => x + 10)); // [11,12,13]

Wrapping up

We started with a barebones implementation of map, then saw how the spec version handles edge cases.
When writing polyfills, you can decide:

Do you want a simple educational version (good for learning)?
Or a full-blown spec-compliant version (good for libraries)?

Writing polyfills is less about memorizing the spec and more about thinking like the language designers. Every time you break down a method into steps, you’re training yourself to reason about edge cases, performance, and developer experience.

Of course, there are still a few corners we haven’t fully handled, things like symbol properties, very large or non-integer lengths, inherited properties, or exotic objects with getters and setters. Don’t worry about all of them now, just knowing they exist gives you a sense of the depth involved in real-world polyfills.

Even I don’t fully understand all of these yet, but that’s the fun part. We can keep exploring and learning together!

Start small, experiment, and refine. Even if your first version isn’t perfect, you’ve already leveled up your understanding.

Till next time!