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 and use deliberately partial implementations to explore several array and string methods. These examples are useful for understanding the mechanics, but a production polyfill also needs the specification's coercion, receiver, sparsevalue, and error semantics together with a conformance test suite.


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, added in ECMAScript 2016, 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:

For a production polyfill, we must implement the official specification rather than only the common path. The examples below are deliberately partial teaching implementations, with their important omissions stated beside each one.

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.

A quick example:

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

Deliberately Partial Implementation

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 teaching implementation handles ordinary numeric indices, but it does not reproduce SameValueZero comparison (so NaN differs), full index coercion, receiver validation, or every sparsearray case. It 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.

Here is a simple example:

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

Deliberately Partial Implementation

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;  };}

This teaching implementation omits receiver and callback validation and the specification's full lengthcoercion rules. Within those limits, it 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.

That looks like this:

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

Deliberately Partial Implementation

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 teaching implementation is limited to ordinary arrays and finite, nonnegative depth values. It does not reproduce generic receivers, full depth coercion, species construction, or every sparsearray rule. It 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.

A reduceRight example:

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

Deliberately Partial Implementation

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;  };}

This teaching implementation does not search past holes for the first present accumulator when no initial value is supplied, and its 32bit length coercion is narrower than the specification. Within those limits, the main righttoleft loop is 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.

A startsWith example:

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

Deliberately Partial Implementation

This teaching implementation handles string inputs and ordinary positions, but it does not perform every specified coercion or reject regular expressions as search values.

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.

An endsWith example:

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

Deliberately Partial Implementation

This teaching implementation covers simple string inputs and lengths, but it does not perform every specified coercion or reject regular expressions as search values.

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.

A padStart example:

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

Deliberately Partial Implementation

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 teaching implementation uses 32bit targetlength coercion and treats an empty padding string as a space, unlike the native method. Within those limits, it 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.

A trim example:

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

Deliberately Partial Implementation

This teaching implementation uses a regularexpression approximation and may not match the exact ECMAScript WhiteSpace and LineTerminator set in every legacy engine.

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.

From Teaching Examples to Production Polyfills

  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 can bridge the gap between modern JavaScript features and older environments, but production compatibility depends on matching the relevant specification and testing its observable edge cases. The deliberately partial examples above show the mechanics behind array methods such as includes and find to string methods such as startsWith and endsWith; they are starting points for study rather than dropin standardscompliant polyfills.

Key Takeaways

  • Production polyfills reproduce modern JavaScript methods in environments where they are unavailable; teaching approximations do not.
  • Writing a production polyfill means following the official specification, including coercion, receiver, sparsevalue, and error behaviour.
  • Always validate polyfills for edge cases to ensure robustness.

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


Need a senior engineer involved?

I can work directly in the codebase, review the architecture, or support the team through delivery when the work needs more than extra hands.