JavaScript Polyfills: Writing Array Methods From Scratch

Before we get into the code, there is something worth knowing about how JavaScript actually works in the browser. Every feature you use, from forEach to Promise, does not just exist out of thin air. There is a massive document called the ECMAScript Specification that lays out how JavaScript should behave. Browser engines like V8 (Chrome), SpiderMonkey (Firefox), and JavaScriptCore (Safari) read those specs and write the actual code that makes your browser understand JavaScript.
The problem is every browser team works at its own speed. When a new version of ECMAScript drops, some browsers implement the new features quickly, others take months. This gap between spec and implementation is where polyfills come in.
What is a Polyfill?
A polyfill is a piece of code that gives older browsers a feature they do not natively support. The term was coined by Remy Sharp around 2009 and has been a part of web development since. Think of it like a compatibility patch: if your browser does not understand a method, the polyfill teaches it.
Every polyfill follows a specific pattern:
if (!SomeObject.prototype.someMethod) {
SomeObject.prototype.someMethod = function() {
// custom implementation here
};
}
The if check matters here. You only want to add the method if the browser does not already have it. You never want to override a working native implementation with your own code.
Before You Write a Polyfill, Ask These Questions
Writing a polyfill is not just about copying behavior, it is about understanding it. Before writing one, work through these questions using the method as a reference. Let us use Array.prototype.forEach as the example:
What does this refer to? Whatever calls the method becomes this. You need to make sure this never becomes null or undefined, especially when you pass callbacks around.
What if the callback is not a function? forEach throws a TypeError if you pass it something that is not a function. Your polyfill should do the same. If someone passes the string "hello" as a callback, execution should stop and throw an error.
Does it handle sparse arrays? Arrays can have gaps: [1, , 3]. The empty slot at index 1 is not undefined, it just does not exist. forEach skips those slots and your polyfill should too.
What does it return? forEach always returns undefined. Other methods like map and filter return a new array. Know the return type before you write a single line.
Does it mutate the original array? forEach does not change the original array. Some methods do. Know which camp yours falls into.
These questions act as a blueprint. Once you answer them, writing the code becomes mechanical.
Understanding Iterative Methods
The array methods covered here are called iterative methods because they loop over an array and run a callback function on each element. According to MDN, they all share a few common behaviors:
The callback receives
(element, index, array)on every callEmpty slots in sparse arrays are skipped
The array length is read once at the start, so adding elements mid-loop does not extend the iteration
They accept an optional
thisArgto control whatthismeans inside the callback
Here is a quick overview of the methods and what they do:
| Method | Returns | Mutates original? | Usage |
|---|---|---|---|
forEach() |
undefined |
No | You want to run side effects on each item |
map() |
New array | No | You want to transform each element |
filter() |
New array | No | You want to keep only matching elements |
find() |
First match or undefined |
No | You want one specific element |
some() |
true or false |
No | You want to check if any element matches |
every() |
true or false |
No | You want to check if all elements match |
reduce() |
Single accumulated value | No | You want to combine all elements into one value |
forEach Polyfill
forEach loops over every element and calls your callback with (element, index, array). It never returns a value.
if (!Array.prototype.myForEach) {
Array.prototype.myForEach = function (callback, thisArg) {
if (typeof callback !== "function") {
throw new TypeError(callback + " is not a function");
}
if (this == null) {
throw new TypeError("Cannot read properties of null or undefined");
}
const arr = this;
const length = arr.length;
for (let i = 0; i < length; i++) {
if (i in arr) {
callback.call(thisArg, arr[i], i, arr);
}
}
return undefined;
};
}
The method is renamed to myForEach so you can verify it works like the original without overriding it.
// Test it
var fruits = ["apple", "banana", "cherry"];
fruits.myForEach(function (fruit, index) {
console.log(index + ": " + fruit);
});
// Output:
// 0: apple
// 1: banana
// 2: cherry
// Sparse array test
const sparse = [1, , 3];
sparse.myForEach((val, i) => console.log(`index: \({i}, value: \){val}`));
// Output:
// index: 0, value: 1
// index: 2, value: 3
Why
i in arr? Theinoperator checks whether an index actually exists in the array, not just whether the value at that index is defined. A sparse array slot andundefinedlook the same when you read the value, butintells you the difference.for...ofiterates over values and would treat empty slots asundefined, which is wrong behavior for this polyfill.
map Polyfill
map works like forEach with one key difference: it collects the return value of every callback and puts it into a brand new array. The original array stays untouched.
if (!Array.prototype.myMap) {
Array.prototype.myMap = function (callback, thisArg) {
if (typeof callback !== "function") {
throw new TypeError(callback + " is not a function");
}
if (this == null) {
throw new TypeError("Array.prototype.myMap called on null or undefined");
}
const arr = this;
const len = arr.length;
const result = new Array(len); // same length as original
for (let i = 0; i < len; i++) {
if (i in arr) {
result[i] = callback.call(thisArg, arr[i], i, arr);
}
}
return result;
};
}
// Test it
const prices = [10, 25, 50, 100];
const withTax = prices.myMap(price => +(price * 0.5).toFixed(2));
console.log(withTax); // [ 5, 12.5, 25, 50 ]
console.log(prices); // [10, 25, 50, 100] — unchanged
// Chaining works because myMap returns an array
const result = [1, 2, 3]
.myMap(n => n * n)
.myMap(n => n + 1);
console.log(result); // [ 2, 5, 10 ]
Notice that result is pre-allocated as new Array(len). This preserves the sparse structure of the original array since empty slots remain empty in the result too.
filter Polyfill
filter is similar to map, but instead of transforming each element, it only keeps elements where the callback returns a truthy value. One detail worth noting: it stores the original value, not the return value of the callback.
if (!Array.prototype.myFilter) {
Array.prototype.myFilter = function (callback, thisArg) {
if (typeof callback !== "function") {
throw new TypeError(callback + " is not a function");
}
if (this == null) {
throw new TypeError("Array.prototype.myFilter called on null or undefined");
}
const arr = Object(this); // handles edge cases like strings
const len = arr.length >>> 0; // coerce to unsigned 32-bit integer
const result = [];
for (let i = 0; i < len; i++) {
if (i in arr) {
if (callback.call(thisArg, arr[i], i, arr)) {
result.push(arr[i]); // push the original value, not the callback result
}
}
}
return result;
};
}
What does
>>> 0do? The>>>is the unsigned right shift operator. When used with0, it coercesarr.lengthto an unsigned 32-bit integer. This means if someone passes a weird length like"10"or-1, it gets converted to a valid number. It is a defensive technique you will see in production-grade polyfills.
// Test it
var prices = [42, 88, 56, 91, 73, 34];
var budget = prices.myFilter(function(price) {
return price <= 50;
});
console.log(budget);
// Output: [ 42, 34 ]
reduce Polyfill
reduce is the most flexible of the bunch. It takes all the elements in an array and "reduces" them down to a single value, accumulating a result with each step.
The extra complexity here is the initial value. If you provide one, the loop starts at index 0 with your value as the accumulator. If you skip it, the first real element in the array becomes the accumulator and the loop starts at index 1.
if (!Array.prototype.myReduce) {
Array.prototype.myReduce = function (callback, initialValue) {
if (typeof callback !== "function") {
throw new TypeError(callback + " is not a function");
}
if (this == null) {
throw new TypeError("Array.prototype.myReduce called on null or undefined");
}
const arr = this;
const len = arr.length;
if (len === 0 && arguments.length < 2) {
throw new TypeError("Reduce of empty array with no initial value");
}
let accumulator;
let startIndex;
if (arguments.length >= 2) {
accumulator = initialValue;
startIndex = 0;
} else {
let initialFound = false;
for (let i = 0; i < len; i++) {
if (i in arr) {
accumulator = arr[i];
startIndex = i + 1;
initialFound = true;
break;
}
}
if (!initialFound) {
throw new TypeError("Reduce of empty array with no initial value");
}
}
for (let i = startIndex; i < len; i++) {
if (i in arr) {
accumulator = callback(accumulator, arr[i], i, arr);
}
}
return accumulator;
};
}
// Add up cart totals
const prices = [99, 49, 159.75, 39.28, 10.25];
const total = prices.myReduce((sum, price) => sum + price, 0);
console.log(total.toFixed(2));
// Output: 357.28
// Group employees by department
const employees = [
{ name: "John", department: "HR" },
{ name: "Jane", department: "Sales" },
{ name: "Harry", department: "HR" },
{ name: "June", department: "Operations" },
];
const grouped = employees.myReduce((acc, item) => {
if (!acc[item.department]) acc[item.department] = [];
acc[item.department].push(item.name);
return acc;
}, {});
console.log(grouped);
// Output: { HR: [ 'John', 'Harry' ], Sales: [ 'Jane' ], Operations: [ 'June' ] }
Always provide an initial value to
reduce. Skipping it on sparse or empty arrays leads to subtle bugs that are hard to track down. The extra argument costs nothing, and the safety it provides is worth it.
Here is the problematic version of the "no initial value" logic that a naive polyfill might use:
// Dangerous — breaks on sparse arrays like [,,1,2,3]
accumulator = arr[0];
startIndex = 1;
The fix used in the polyfill above loops through the array to find the first existing index, which correctly handles sparse arrays.
The Bigger Picture: When to Actually Use Polyfills
Writing polyfills is a great learning exercise, and they appear regularly in technical interviews. However, rolling your own polyfill in production code is not a good idea. Here is what to keep in mind:
Always guard with a feature check. Never override a native method. The if (!SomeObject.prototype.someMethod) check at the top of every polyfill exists for this reason.
Use core-js or Babel in production. These are battle-tested libraries that handle polyfilling correctly across hundreds of edge cases. core-js is the industry standard. Babel can automatically inject polyfills based on your target browser list.
Check browser support first. Before adding a polyfill, visit Can I Use and check if your target browsers already support the feature. Every polyfill you add increases the bundle size, so do not add them unnecessarily.
Read the MDN docs for the method you are polyfilling. The behavior section and edge case notes on MDN are the closest thing to a checklist for getting the implementation right. The ECMAScript specification goes even deeper if you need it.
Conclusion
Polyfills exist because the web moves faster than browsers can keep up. The ECMAScript spec defines new features, but not every browser implements them at the same time. Understanding how to bridge that gap, whether by writing your own or using tools like Babel and core-js, is part of knowing how JavaScript actually works.
The four polyfills in this post cover the most common iterative array methods: forEach, map, filter, and reduce. Writing them yourself, even just once, teaches you things about this, sparse arrays, arguments, and prototype methods that reading documentation alone does not. The next time you use [1,2,3].map(...), you will have a clearer picture of what is happening underneath.




