Building Polyfills for JavaScript Array and String Methods

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, sparse‑value, 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 if‑not:
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 real‑world 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); // trueDeliberately 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 sparse‑array case. It works by:
Handle the
fromIndexParameter:- If
fromIndexis 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
fromIndexis ‑2, the starting index becomes5 - 2 = 3.
- If the array has a length of 5 and
- If
fromIndexis positive orundefined, it defaults to0or the provided value.
- If
Iterate Over the Array
:- Using a
forloop, the code begins checking each element in the array starting from the calculated index (start).
- Using a
Check for Equality
:- Inside the loop, each element is compared to
valueToFindusing strict equality (===). This ensures the comparison respects both value and type.- For example,
3 === "3"would returnfalse.
- For example,
- Inside the loop, each element is compared to
Return
trueif a Match is Found:- If the value is found during iteration, the function immediately returns
true.
- If the value is found during iteration, the function immediately returns
Return
falseif No Match is Found:- If the loop completes without finding a match, the function returns
false.
- If the loop completes without finding a match, the function returns
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); // 12Deliberately 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 length‑coercion rules. Within those limits, it works by:
Iterate Over the Array
:- Using a
forloop, the code applies the callback function to each element in the array.
- Using a
Apply the Callback
:- The
callbackfunction is invoked with the current element, index, and the array itself. If the callback returnstrue, the iteration stops, and the current element is returned.
- The
Handle
thisArgContext:- The optional
thisArgparameter allows the callback to execute in a specific context using.call().
- The optional
Return
undefinedif No Match is Found:- If no elements satisfy the callback, the function returns
undefined.
- If no elements satisfy the callback, the function returns
3. Array.prototype.flat
The flat method creates a new array with all sub‑array 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, non‑negative depth values. It does not reproduce generic receivers, full depth coercion, species construction, or every sparse‑array rule. It works by:
Default Depth
:- If no depth is specified, it defaults to
1.
- If no depth is specified, it defaults to
Recursive Flattening
:- The
flattenfunction recursively reduces the array, concatenating nested arrays if the depth (d) is greater than0.
- The
Preserve Original
:- When
depthis0, the array is returned as‑is usingslice.
- When
4. Array.prototype.reduceRight
The reduceRight method applies a function against an accumulator and each value of the array (from right‑to‑left) 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 32‑bit length coercion is narrower than the specification. Within those limits, the main right‑to‑left loop is straightforward:
Right‑to‑Left Iteration
:- The loop starts at the last element and moves leftward.
Handle Initial Value
:- If
initialValueis not provided, the last element of the array becomes the initial accumulator.
- If
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"); // trueDeliberately 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
substringto extract a portion of the string starting atpositionand ending atposition + search.length.
- The method uses
Compare Substrings
:- It compares the extracted substring with the
searchstring.
- It compares the extracted substring with the
Default ****
position:- If no
positionis specified, it defaults to0.
- If no
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!"); // trueDeliberately 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
lengthParameter:- If
lengthis specified, the string is truncated usingsubstringto the given length.
- If
Extract Substring
:- The method uses
sliceto extract the ending portion of the string with the same length assearch.
- The method uses
Compare Substrings
:- It compares the extracted portion with the
searchstring to determine if they match.
- It compares the extracted portion with the
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 32‑bit target‑length 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
repeatto generate a padding string long enough andsubstringto trim it to the exact required length.
- Uses
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 regular‑expression 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.
- Matches and removes leading (
Replace Operation
:- Utilises
replaceto return the string without the matched whitespace.
- Utilises
From Teaching Examples to Production Polyfills
Check for Native Support
: Always ensure the method is not already implemented in the environment before adding a polyfill.Preserve Standards
: Follow the official specification when writing polyfills to avoid unexpected behaviour.Avoid Overwriting
: Only define the polyfill if the method is truly missing.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 drop‑in standards‑compliant 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, sparse‑value, and error behaviour.
- Always validate polyfills for edge cases to ensure robustness.
By mastering polyfills, you enhance your JavaScript projects' compatibility and longevity.