Building Polyfills for JavaScript Array and String Methods

Hero image for Building Polyfills for JavaScript Array and String Methods. Image by Brook Anderson.
Hero image for 'Building Polyfills for JavaScript Array and String Methods.' Image by Brook Anderson.

Modern JavaScript includes a wide range of array and string methods that simplify coding and improve the readability of our code. However, older browsers or specific environments often lack support for some of these methods. This is where polyfills come in; they allow us to implement equivalent functionality, bridging gaps in browser support.

In this article, I intend to explain the concept of polyfills, explore some commonly missing JavaScript methods, and guide you through building polyfills for them. By the end, you should hopefully feel confident in both understanding and writing polyfills to ensure your code runs seamlessly across environments.


What are Polyfills?

A polyfill is a piece of JavaScript code that replicates the functionality of a newer method or feature, enabling its use in environments that do not support it natively. For example, the Array.prototype.includes method, introduced in ES6, might not be available in older browsers. By writing a polyfill, we can mimic its behaviour so that we can still use it and our code doesn't break when loaded on those older browsers.

Polyfills are distinct from shims. Whilst both fill in missing functionality, a shim modifies or extends native objects, often with custom behaviour. Polyfills strictly replicate the expected functionality to maintain standards.

Creating a Polyfill: The General Approach

When creating a polyfill, there's a fairly simple and structured approach that we can follow:

1. Check for Native Support:

Before defining a polyfill, we need to check if the method already exists within that environment. This prevents unnecessary overwriting of native implementations. Thus, we wrap our polyfill in an ifnot:

if (!Array.prototype.includes) {    // Our polyfill goes here}

2. Define the Functionality:

We then implement the behaviour as per the official specification, and overriding the (missing) native implementation. It's really important to ensure that the polyfill we write exactly replicates the functionality of the native method.

Array.prototype.includes = function(valueToFind, fromIndex) {    // Polyfill logic goes in here};

3. Handle Edge Cases:

With our polyfill written, we need to make sure that we also handle special cases such as handling negative indices or optional parameters, to ensure complete compatibility with realworld usage.

4. Test Extensively:

It goes without saying that the final step is always going to be that it needs to be validated against various test cases to confirm that it behaves identically to the native method. This is also important in areas of the application where these polyfills might get used manual testing across devices will hopefully quickly surface any inconsistencies.


Array Method Polyfills

Starting with array methods, here are some of the most common ones for which I've had to write polyfills in the past.

1. Array.prototype.includes

The includes method determines if an array contains a specific value or not. It returns true if the value is found and false otherwise.

For example:

const numbers = [1, 2, 3];numbers.includes(2);  // true

The Polyfill

if (!Array.prototype.includes) {  Array.prototype.includes = function(valueToFind, fromIndex) {    // Convert negative index to the correct starting position    const start = fromIndex < 0 ? Math.max(this.length + fromIndex, 0) : fromIndex || 0;    for (let i = start; i < this.length; i++) {      if (this[i] === valueToFind) return true;    }    return false;  };}

This works by:

  • Handle the
    fromIndex Parameter:
    • If fromIndex is a negative number, the code calculates the actual starting index by adding the negative value to the array's length. For example:
      • If the array has a length of 5 and fromIndex is 2, the starting index becomes 5 2 = 3.
    • If fromIndex is positive or undefined, it defaults to 0 or the provided value.
  • Iterate Over the Array
    :
    • Using a for loop, the code begins checking each element in the array starting from the calculated index (start).
  • Check for Equality
    :
    • Inside the loop, each element is compared to valueToFind using strict equality (===). This ensures the comparison respects both value and type.
      • For example, 3 === "3" would return false.
  • Return
    true if a Match is Found:
    • If the value is found during iteration, the function immediately returns true.
  • Return
    false if No Match is Found:
    • If the loop completes without finding a match, the function returns false.

2. Array.prototype.find

The find method returns the first element in an array that satisfies a provided testing function. If no elements match then it returns undefined.

For example:

const numbers = [5, 12, 8];const found = numbers.find(num => num > 10);  // 12

The Polyfill

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

Fortunately, this is a little more straightforward than the last polyfill! This works by:

  • Iterate Over the Array
    :
    • Using a for loop, the code applies the callback function to each element in the array.
  • Apply the Callback
    :
    • The callback function is invoked with the current element, index, and the array itself. If the callback returns true, the iteration stops, and the current element is returned.
  • Handle
    thisArg Context:
    • The optional thisArg parameter allows the callback to execute in a specific context using .call().
  • Return
    undefined if No Match is Found:
    • If no elements satisfy the callback, the function returns undefined.

3. Array.prototype.flat

The flat method creates a new array with all subarray elements concatenated into it, up to the specified depth.

For example:

const arr = [1, [2, [3, [4]]]];arr.flat(2);  // [1, 2, 3, [4]]

The Polyfill

if (!Array.prototype.flat) {  Array.prototype.flat = function(depth = 1) {    const flatten = (arr, d) => {      return d > 0        ? arr.reduce(            (acc, val) => acc.concat(Array.isArray(val) ? flatten(val, d - 1) : val),            []          )        : arr.slice();    };    return flatten(this, depth);  };}

This works by:

  • Default Depth
    :
    • If no depth is specified, it defaults to 1.
  • Recursive Flattening
    :
    • The flatten function recursively reduces the array, concatenating nested arrays if the depth (d) is greater than 0.
  • Preserve Original
    :
    • When depth is 0, the array is returned asis using slice.

4. Array.prototype.reduceRight

The reduceRight method applies a function against an accumulator and each value of the array (from righttoleft) to reduce it to a single value.

For example:

const arr = ['right', 'to', 'reduce'];arr.reduceRight((acc, curr) => `${acc} ${curr}`, '');  // " reduce to right"

The Polyfill

if (!Array.prototype.reduceRight) {  Array.prototype.reduceRight = function(callback, initialValue) {    if (this == null) {      throw new TypeError('Array.prototype.reduceRight called on null or undefined');    }    if (typeof callback !== 'function') {      throw new TypeError(callback + ' is not a function');    }    const array = Object(this);    let length = array.length >>> 0;    let index = length - 1;    let value = initialValue;    if (arguments.length < 2) {      if (length === 0) {        throw new TypeError('Reduce of empty array with no initial value');      }      value = array[index--];    }    for (; index >= 0; index--) {      if (index in array) {        value = callback(value, array[index], index, array);      }    }    return value;  };}

From a code perspective, this is quite possibly one of the more complex ones to write, however, the explanation is actually quite straightforward!

  • RighttoLeft Iteration
    :
    • The loop starts at the last element and moves leftward.
  • Handle Initial Value
    :
    • If initialValue is not provided, the last element of the array becomes the initial accumulator.
  • Edge Cases
    :
    • The polyfill throws appropriate errors for empty arrays without an initial value.

String Method Polyfills

1. String.prototype.startsWith

The startsWith method checks if a string begins with a specific substring.

For example:

const str = "Hello, world!";str.startsWith("Hello");  // true

The Polyfill

Even without the method, this is a fairly straightforward one to replicate:

if (!String.prototype.startsWith) {  String.prototype.startsWith = function(search, position) {    position = position || 0;    return this.substring(position, position + search.length) === search;  };}

This works by:

  • Extract Substring
    :
    • The method uses substring to extract a portion of the string starting at position and ending at position + search.length.
  • Compare Substrings
    :
    • It compares the extracted substring with the search string.
  • Default ****
    position:
    • If no position is specified, it defaults to 0.

2. String.prototype.endsWith

The exact opposite of startsWith, the endsWith method checks if a string ends with a specific substring.

For example:

const str = "Hello, world!";str.endsWith("world!");  // true

The Polyfill

Again, being fairly basic string manipulation, this is an easy one to replicate:

if (!String.prototype.endsWith) {  String.prototype.endsWith = function(search, length) {    const str = length !== undefined ? this.substring(0, length) : this;    return str.slice(-search.length) === search;  };}

This works by:

  • Handle the
    length Parameter:
    • If length is specified, the string is truncated using substring to the given length.
  • Extract Substring
    :
    • The method uses slice to extract the ending portion of the string with the same length as search.
  • Compare Substrings
    :
    • It compares the extracted portion with the search string to determine if they match.

3. String.prototype.padStart

The padStart method pads the current string with another string until the resulting string reaches the given length.

For example:

'5'.padStart(3, '0');  // "005"

The Polyfill

if (!String.prototype.padStart) {  String.prototype.padStart = function(targetLength, padString) {    targetLength = targetLength >> 0;  // Floor the target length    padString = String(padString || ' ');    if (this.length >= targetLength) {      return String(this);    }    const padLength = targetLength - this.length;    return padString.repeat(Math.ceil(padLength / padString.length)).substring(0, padLength) + String(this);  };}

This works by:

  • Calculate Pad Length
    :
    • Determines how many padding characters are needed to meet the target length.
  • Repeat and Trim
    :
    • Uses repeat to generate a padding string long enough and substring to trim it to the exact required length.
  • Preserve Original
    :
    • If the string is already equal to or longer than the target length, it returns the original string.

4. String.prototype.trim

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

For example:

'  hello world  '.trim();  // "hello world"

The Polyfill

With regex, this is an incredibly straightforward one to replicate:

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

This works by:

  • Regex for Trimming

    :
    • Matches and removes leading (^\s+) and trailing (\s+$) whitespace.
  • Replace Operation

    :
    • Utilises replace to return the string without the matched whitespace.

Writing Better Polyfills: Best Practices

  1. Check for Native Support

    : Always ensure the method is not already implemented in the environment before adding a polyfill.
  2. Preserve Standards

    : Follow the official specification when writing polyfills to avoid unexpected behaviour.
  3. Avoid Overwriting

    : Only define the polyfill if the method is truly missing.
  4. Test Extensively

    : Validate your polyfills against edge cases to ensure reliability.

Wrapping up

Polyfills bridge the gap between modern JavaScript features and older environments. By understanding how to write and implement them, you ensure your code remains functional across a variety of platforms. From array methods like includes and find to string methods such as startsWith and endsWith, polyfills allow you to maintain compatibility without sacrificing functionality.

Key Takeaways

  • Polyfills replicate modern JavaScript methods in environments where they are unavailable.
  • Writing a polyfill involves adhering to official method specifications and preserving native functionality.
  • Always validate polyfills for edge cases to ensure robustness.

By mastering polyfills, you enhance your JavaScript projects' compatibility and longevity.


Categories:

  1. Development
  2. Front‑End Development
  3. Guides
  4. JavaScript