Skip to main content

Command Palette

Search for a command to run...

Mastering JavaScript Polyfills

Published
15 min read
Mastering JavaScript Polyfills
S

Full-stack developer documenting what I’m learning as I go. This space is all about tech, understanding how things work, and writing things down as they start to make sense.

Introduction

Polyfills are crucial pieces of code that allow developers to use modern JavaScript features in environments that don't natively support them. In this comprehensive tutorial, we'll explore how to create polyfills for commonly used Array and Promise methods, understanding their internal workings along the way.

By the end of this guide, you'll have a deep understanding of how these built-in methods work under the hood and how to implement them from scratch.

Part 1: Array Methods Polyfills

1. Array.prototype.forEach()

The forEach() method executes a provided function once for each array element. Unlike map(), it doesn't return a new array.

How it Works

  • Iterates through each element in the array

  • Executes the callback function for each element

  • Handles sparse arrays (skips missing indices)

  • Returns undefined

Implementation

Array.prototype.myForEach = function (callback) {
  if (this == null) {
    throw new TypeError(
      "Array.prototype.myForEach called on null or undefined"
    );
  }

  if (typeof callback !== "function") {
    throw new TypeError(callback + " is not a function");
  }

  const obj = Object(this);
  const len = obj.length >>> 0; 

  for (let i = 0; i < len; i++) {
    if (i in obj) {
      callback(obj[i], i, obj);
    }
  }
};

Example Usage

const arr = [1, 2, 3, 4, 5];

arr.myForEach((element, index, self) => {
  console.log(`Index: ${index}, Element: ${element}`);
});

Key Concepts

  • this == null: Checks for both null and undefined

  • obj.length >>> 0: The zero-fill right shift ensures the length is a valid unsigned 32-bit integer

  • i in obj: Checks if the property exists, handling sparse arrays correctly


2. Array.prototype.map()

The map() method creates a new array with the results of calling a provided function on every element.

How it Works

  • Creates a new array of the same length

  • Transforms each element using the callback function

  • Preserves sparse array structure

  • Returns the new array

Implementation

Array.prototype.myMap = function (callback) {
  if (this == null) {
    throw new TypeError("Array.prototype.myMap called on null or undefined");
  }

  if (typeof callback !== "function") {
    throw new TypeError(callback + " is not a function");
  }

  const obj = Object(this);
  const len = obj.length >>> 0;
  const newArray = new Array(len);

  for (let i = 0; i < len; i++) {
    if (i in obj) {
      newArray[i] = callback(obj[i], i, obj);
    }
  }

  return newArray;
};

Example Usage

const numbers = [1, 2, 3, 4, 5];

const doubled = numbers.myMap((item, idx, self) => {
  return item * 2;
});

console.log(doubled); // [2, 4, 6, 8, 10]

const squared = numbers.myMap(item => item ** 2);
console.log(squared); // [1, 4, 9, 16, 25]

Real-World Example

const users = [
  { id: 1, name: "Alice", age: 25 },
  { id: 2, name: "Bob", age: 30 },
  { id: 3, name: "Charlie", age: 35 }
];

const userNames = users.myMap(user => user.name);
console.log(userNames); // ["Alice", "Bob", "Charlie"]

3. Array.prototype.filter()

The filter() method creates a new array with all elements that pass the test implemented by the provided function.

How it Works

  • Tests each element with the callback function

  • Includes elements that return true

  • Creates a new array (original array unchanged)

  • Returns empty array if no elements pass

Implementation

Array.prototype.myFilter = function (callback) {
  if (this == null) {
    throw new TypeError("Array.prototype.myFilter called on null or undefined");
  }

  if (typeof callback !== "function") {
    throw new TypeError(callback + " is not a function");
  }

  const obj = Object(this);
  const len = obj.length >>> 0;
  const newArray = [];

  for (let i = 0; i < len; i++) {
    if (i in obj) {
      if (callback(obj[i], i, obj)) {
        newArray.push(obj[i]);
      }
    }
  }

  return newArray;
};

Example Usage

const numbers = [1, 2, 3, 4, 5, 6];

const evenNumbers = numbers.myFilter((item, idx, self) => {
  return item % 2 === 0;
});

console.log(evenNumbers); // [2, 4, 6]

const greaterThanThree = numbers.myFilter(item => item > 3);
console.log(greaterThanThree); // [4, 5, 6]

Real-World Example

const products = [
  { name: "Laptop", price: 1000, inStock: true },
  { name: "Phone", price: 500, inStock: false },
  { name: "Tablet", price: 300, inStock: true },
  { name: "Monitor", price: 200, inStock: true }
];

const availableProducts = products.myFilter(product => product.inStock);
console.log(availableProducts);
// [{ name: "Laptop", ... }, { name: "Tablet", ... }, { name: "Monitor", ... }]

const affordableProducts = products.myFilter(product => product.price <= 500);
console.log(affordableProducts);

4. Array.prototype.find()

The find() method returns the first element in the array that satisfies the provided testing function.

How it Works

  • Iterates through the array

  • Returns the first element where callback returns true

  • Returns undefined if no element is found

  • Stops iteration once element is found

Implementation

Array.prototype.myFind = function (callback) {
  if (this == null) {
    throw new TypeError("Array.prototype.myFind called on null or undefined");
  }

  if (typeof callback !== "function") {
    throw new TypeError(callback + " is not a function");
  }

  const obj = Object(this);
  const len = obj.length >>> 0;

  for (let i = 0; i < len; i++) {
    if (i in obj) {
      if (callback(obj[i], i, obj)) {
        return obj[i];
      }
    }
  }

  return undefined;
};

Example Usage

const employees = [
  { empId: 1, name: "Alice", department: "Engineering" },
  { empId: 2, name: "Bob", department: "Sales" },
  { empId: 3, name: "Charlie", department: "Engineering" }
];

const employee = employees.myFind((item, idx, self) => {
  return item.empId === 2;
});

console.log(employee); // { empId: 2, name: "Bob", department: "Sales" }

const engineer = employees.myFind(emp => emp.department === "Engineering");
console.log(engineer); // { empId: 1, name: "Alice", department: "Engineering" }

5. Array.prototype.some()

The some() method tests whether at least one element in the array passes the test implemented by the provided function.

How it Works

  • Returns true if at least one element passes the test

  • Returns false if all elements fail the test

  • Short-circuits (stops) when first true is found

  • Returns false for empty arrays

Implementation

Array.prototype.mySome = function (callback) {
  if (this == null) {
    throw new TypeError("Array.prototype.mySome called on null or undefined");
  }

  if (typeof callback !== "function") {
    throw new TypeError(callback + " is not a function");
  }

  const obj = Object(this);
  const len = obj.length >>> 0;

  for (let i = 0; i < len; i++) {
    if (i in obj) {
      if (callback(obj[i], i, obj)) {
        return true;
      }
    }
  }

  return false;
};

Example Usage

const numbers = [1, 2, 3, 4, 5, 10];

const hasLargeNumber = numbers.mySome((item, idx, self) => {
  return item > 8;
});

console.log(hasLargeNumber); // true

const hasNegative = numbers.mySome(item => item < 0);
console.log(hasNegative); // false

Real-World Example

const orders = [
  { id: 1, status: "pending", total: 100 },
  { id: 2, status: "shipped", total: 200 },
  { id: 3, status: "delivered", total: 150 }
];

const hasPendingOrders = orders.mySome(order => order.status === "pending");
console.log(hasPendingOrders); // true

const hasExpensiveOrder = orders.mySome(order => order.total > 500);
console.log(hasExpensiveOrder); // false

6. Array.prototype.every()

The every() method tests whether all elements in the array pass the test implemented by the provided function.

How it Works

  • Returns true only if all elements pass the test

  • Returns false if any element fails

  • Short-circuits when first false is found

  • Returns true for empty arrays

Implementation

Array.prototype.myEvery = function (callback) {
  if (this == null) {
    throw new TypeError("Array.prototype.myEvery called on null or undefined");
  }

  if (typeof callback !== "function") {
    throw new TypeError(callback + " is not a function");
  }

  const obj = Object(this);
  const len = obj.length >>> 0;

  for (let i = 0; i < len; i++) {
    if (i in obj) {
      if (!callback(obj[i], i, obj)) {
        return false;
      }
    }
  }

  return true;
};

Example Usage

const numbers = [1, 2, 3, 4, 5];

const allPositive = numbers.myEvery((item, idx, self) => {
  return item > 0;
});

console.log(allPositive); // true

const allGreaterThanOne = numbers.myEvery(item => item > 1);
console.log(allGreaterThanOne); // false

Real-World Example

const users = [
  { name: "Alice", age: 25, verified: true },
  { name: "Bob", age: 30, verified: true },
  { name: "Charlie", age: 35, verified: true }
];

const allVerified = users.myEvery(user => user.verified);
console.log(allVerified); // true

const allAdults = users.myEvery(user => user.age >= 18);
console.log(allAdults); // true

7. Array.prototype.reduce()

The reduce() method executes a reducer function on each element of the array, resulting in a single output value.

How it Works

  • Accumulates values into a single result

  • Takes an optional initial value

  • If no initial value, uses first array element

  • Processes array left to right

Implementation

Array.prototype.myReduce = function (callback, initialValue) {
  if (this == null) {
    throw new TypeError("Array.prototype.myReduce called on null or undefined");
  }

  if (typeof callback !== "function") {
    throw new TypeError(callback + " is not a function");
  }

  const obj = Object(this);
  const len = obj.length >>> 0;

  let accumulator;
  let startIdx = 0;

  if (arguments.length > 1) {
    accumulator = initialValue;
  } else {
    // Find first existing element
    while (startIdx < len && !(startIdx in obj)) {
      startIdx++;
    }

    if (startIdx >= len) {
      throw new TypeError("Reduce of empty array with no initial value");
    }
    accumulator = obj[startIdx++];
  }

  for (let i = startIdx; i < len; i++) {
    if (i in obj) {
      accumulator = callback(accumulator, obj[i], i, obj);
    }
  }

  return accumulator;
};

Example Usage

const numbers = [1, 2, 3, 4, 5];

// Sum all numbers
const sum = numbers.myReduce((acc, curr, idx, self) => {
  return acc + curr;
}, 0);

console.log(sum); // 15

// Product of all numbers
const product = numbers.myReduce((acc, curr) => acc * curr, 1);
console.log(product); // 120

Real-World Examples

// Counting occurrences
const fruits = ["apple", "banana", "apple", "orange", "banana", "apple"];

const fruitCount = fruits.myReduce((acc, fruit) => {
  acc[fruit] = (acc[fruit] || 0) + 1;
  return acc;
}, {});

console.log(fruitCount);
// { apple: 3, banana: 2, orange: 1 }

// Flattening nested arrays
const nested = [[1, 2], [3, 4], [5, 6]];

const flattened = nested.myReduce((acc, curr) => {
  return acc.concat(curr);
}, []);

console.log(flattened); // [1, 2, 3, 4, 5, 6]

// Calculating total cart value
const cart = [
  { item: "Laptop", price: 1000, quantity: 1 },
  { item: "Mouse", price: 25, quantity: 2 },
  { item: "Keyboard", price: 75, quantity: 1 }
];

const totalCost = cart.myReduce((total, product) => {
  return total + (product.price * product.quantity);
}, 0);

console.log(totalCost); // 1125

8. Array.prototype.flat()

The flat() method creates a new array with all sub-array elements concatenated into it recursively up to the specified depth.

How it Works

  • Recursively flattens nested arrays

  • Takes a depth parameter (default: 1)

  • Can use Infinity to flatten all levels

  • Handles sparse arrays

Implementation

Array.prototype.myFlat = function (depth) {
  if (this == null) {
    throw new TypeError("Array.prototype.myFlat called on null or undefined");
  }

  let maxDepth;
  if (depth === undefined) {
    maxDepth = 1;
  } else if (depth === Infinity) {
    maxDepth = Infinity;
  } else {
    maxDepth = Math.floor(Number(depth));
  }

  const newArray = [];
  const sourceArray = Object(this);
  const len = sourceArray.length >>> 0;

  function flatHelper(arr, currDepth) {
    for (let i = 0; i < arr.length; i++) {
      if (i in arr) {
        if (Array.isArray(arr[i]) && currDepth < maxDepth) {
          flatHelper(arr[i], currDepth + 1);
        } else {
          newArray.push(arr[i]);
        }
      }
    }
  }

  flatHelper(sourceArray, 0);
  return newArray;
};

Example Usage

const arr = [1, 2, [3, 4, [5, 6, 7]], 8];

// Flatten one level
const flat1 = arr.myFlat(1);
console.log(flat1); // [1, 2, 3, 4, [5, 6, 7], 8]

// Flatten two levels
const flat2 = arr.myFlat(2);
console.log(flat2); // [1, 2, 3, 4, 5, 6, 7, 8]

// Flatten all levels
const flatAll = arr.myFlat(Infinity);
console.log(flatAll); // [1, 2, 3, 4, 5, 6, 7, 8]

Real-World Example

const nestedCategories = [
  "Electronics",
  ["Computers", ["Laptops", "Desktops"]],
  ["Phones", ["iPhone", "Android"]],
  "Accessories"
];

const allCategories = nestedCategories.myFlat(Infinity);
console.log(allCategories);
// ["Electronics", "Computers", "Laptops", "Desktops", "Phones", "iPhone", "Android", "Accessories"]

Part 2: Promise Methods Polyfills

1. Promise.all()

Promise.all() takes an iterable of promises and returns a single Promise that resolves when all promises resolve, or rejects when any promise rejects.

How it Works

  • Waits for all promises to resolve

  • Rejects immediately if any promise rejects

  • Resolves with an array of results in order

  • Returns empty array for empty input

Implementation

Promise.myAll = function (iterable) {
  return new Promise((resolve, reject) => {
    const promises = Array.from(iterable);
    const len = promises.length;

    if (len === 0) {
      resolve([]);
      return;
    }

    const results = new Array(len);
    let resolvedCount = 0;

    promises.forEach((promise, index) => {
      Promise.resolve(promise)
        .then((value) => {
          results[index] = value;
          resolvedCount++;

          if (resolvedCount === len) {
            resolve(results);
          }
        })
        .catch((error) => {
          reject(error);
        });
    });
  });
};

Example Usage

const promise1 = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("Promise 1 resolved");
    }, 1000);
  });
};

const promise2 = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("Promise 2 resolved");
    }, 2000);
  });
};

const promise3 = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("Promise 3 resolved");
    }, 1500);
  });
};

Promise.myAll([promise1(), promise2(), promise3()])
  .then((results) => {
    console.log("All resolved:", results);
    // All resolved: ["Promise 1 resolved", "Promise 2 resolved", "Promise 3 resolved"]
  })
  .catch((error) => {
    console.error("One rejected:", error);
  });

Real-World Example

// Fetching multiple API endpoints
const fetchUser = fetch('/api/user/1').then(res => res.json());
const fetchPosts = fetch('/api/posts').then(res => res.json());
const fetchComments = fetch('/api/comments').then(res => res.json());

Promise.myAll([fetchUser, fetchPosts, fetchComments])
  .then(([user, posts, comments]) => {
    console.log('User:', user);
    console.log('Posts:', posts);
    console.log('Comments:', comments);
  })
  .catch(error => {
    console.error('Failed to fetch data:', error);
  });

2. Promise.allSettled()

Promise.allSettled() waits for all promises to settle (either resolve or reject) and returns their results.

How it Works

  • Waits for all promises to settle

  • Never rejects (always resolves)

  • Returns array of objects with status and value/reason

  • Useful when you need all results regardless of success

Implementation

Promise.myAllSettled = function (iterable) {
  return new Promise((resolve, reject) => {
    const promises = Array.from(iterable);
    const len = promises.length;

    if (len === 0) {
      resolve([]);
      return;
    }

    const results = new Array(len);
    let settledCount = 0;

    promises.forEach((promise, index) => {
      Promise.resolve(promise)
        .then((value) => {
          results[index] = { status: "fulfilled", value: value };
        })
        .catch((reason) => {
          results[index] = { status: "rejected", reason: reason };
        })
        .finally(() => {
          settledCount++;
          if (settledCount === len) {
            resolve(results);
          }
        });
    });
  });
};

Example Usage

const promise1 = () => {
  return new Promise((resolve) => {
    setTimeout(() => resolve("Success 1"), 1000);
  });
};

const promise2 = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => reject("Error 2"), 1000);
  });
};

const promise3 = () => {
  return new Promise((resolve) => {
    setTimeout(() => resolve("Success 3"), 1000);
  });
};

Promise.myAllSettled([promise1(), promise2(), promise3()])
  .then((results) => {
    console.log("All settled:", results);
    /* Output:
    [
      { status: "fulfilled", value: "Success 1" },
      { status: "rejected", reason: "Error 2" },
      { status: "fulfilled", value: "Success 3" }
    ]
    */
  });

Real-World Example

// Processing multiple independent operations
const operations = [
  saveUserProfile(userData),
  uploadProfilePicture(imageFile),
  sendWelcomeEmail(email),
  createUserDirectory(userId)
];

Promise.myAllSettled(operations)
  .then((results) => {
    results.forEach((result, index) => {
      if (result.status === "fulfilled") {
        console.log(`Operation ${index + 1} succeeded:`, result.value);
      } else {
        console.error(`Operation ${index + 1} failed:`, result.reason);
      }
    });
  });

3. Promise.race()

Promise.race() returns a promise that resolves or rejects as soon as one of the promises resolves or rejects.

How it Works

  • Returns result of first settled promise

  • Can resolve or reject based on first result

  • Other promises continue but results are ignored

  • Useful for timeouts and quick responses

Implementation

Promise.myRace = function (iterable) {
  return new Promise((resolve, reject) => {
    const promises = Array.from(iterable);
    const len = promises.length;

    if (len === 0) {
      return;
    }

    promises.forEach((promise) => {
      Promise.resolve(promise)
        .then((value) => {
          resolve(value);
        })
        .catch((reason) => {
          reject(reason);
        });
    });
  });
};

Example Usage

const fast = new Promise((resolve) => {
  setTimeout(() => resolve("Fast promise won"), 100);
});

const slow = new Promise((resolve) => {
  setTimeout(() => resolve("Slow promise"), 1000);
});

const failure = new Promise((resolve, reject) => {
  setTimeout(() => reject("Failed promise"), 500);
});

Promise.myRace([fast, slow, failure])
  .then((result) => {
    console.log("Winner:", result); // "Fast promise won"
  })
  .catch((error) => {
    console.error("First to fail:", error);
  });

Real-World Example

// Implementing request timeout
function fetchWithTimeout(url, timeout = 5000) {
  const fetchPromise = fetch(url);

  const timeoutPromise = new Promise((_, reject) => {
    setTimeout(() => reject(new Error("Request timeout")), timeout);
  });

  return Promise.myRace([fetchPromise, timeoutPromise]);
}

fetchWithTimeout('/api/data', 3000)
  .then(response => response.json())
  .then(data => console.log('Data:', data))
  .catch(error => console.error('Error:', error.message));

4. Promise.any()

Promise.any() takes an iterable of promises and returns a promise that resolves when any of the promises resolves, or rejects if all promises reject.

How it Works

  • Resolves with first successful promise

  • Ignores rejected promises

  • Rejects only if all promises reject

  • Returns AggregateError with all rejection reasons

Implementation

Promise.myAny = function (iterable) {
  return new Promise((resolve, reject) => {
    const promises = Array.from(iterable);
    const len = promises.length;

    if (len === 0) {
      reject(new AggregateError([], "All promises were rejected"));
      return;
    }

    const errors = new Array(len);
    let rejectCount = 0;

    promises.forEach((promise, index) => {
      Promise.resolve(promise)
        .then((value) => {
          resolve(value);
        })
        .catch((reason) => {
          rejectCount++;
          errors[index] = reason;
          if (rejectCount === len) {
            reject(new AggregateError(errors, "All promises were rejected"));
          }
        });
    });
  });
};

Example Usage

const promise1 = () => {
  return new Promise((resolve) => {
    setTimeout(() => resolve("First success"), 2000);
  });
};

const promise2 = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => reject("Second failed"), 1000);
  });
};

const promise3 = () => {
  return new Promise((resolve) => {
    setTimeout(() => resolve("Third success"), 1000);
  });
};

Promise.myAny([promise1(), promise2(), promise3()])
  .then((result) => {
    console.log("First success:", result); // "Third success"
  })
  .catch((error) => {
    console.error("All failed:", error);
  });

Real-World Example

// Trying multiple API endpoints
function fetchFromMultipleSources(endpoints) {
  const promises = endpoints.map(endpoint => 
    fetch(endpoint).then(res => res.json())
  );

  return Promise.myAny(promises);
}

const apiEndpoints = [
  'https://api1.example.com/data',
  'https://api2.example.com/data',
  'https://api3.example.com/data'
];

fetchFromMultipleSources(apiEndpoints)
  .then(data => {
    console.log('Successfully fetched data:', data);
  })
  .catch(error => {
    console.error('All endpoints failed:', error);
  });

Key Differences Between Promise Methods

MethodResolves WhenRejects WhenUse Case
Promise.all()All promises resolveAny promise rejectsAll operations must succeed
Promise.allSettled()All promises settleNever rejectsNeed all results regardless
Promise.race()First promise settlesFirst promise rejectsNeed fastest response
Promise.any()First promise resolvesAll promises rejectNeed any successful result

Conclusion

Understanding these polyfills gives you deeper insight into how JavaScript works internally. These implementations follow the ECMAScript specifications and handle edge cases properly.

Key takeaways:

  • Array methods transform data without mutating originals

  • Promise methods coordinate asynchronous operations

  • Proper error handling is crucial

  • Understanding these implementations makes you a better JavaScript developer

Practice implementing these polyfills yourself and experiment with different use cases to solidify your understanding!