Polyfills

Difficulty: Advanced

A polyfill is a piece of code that provides functionality that a browser does not natively support. Writing polyfills for built-in JavaScript methods is one of the most popular interview exercises because it tests your understanding of how these methods actually work under the hood. It requires knowledge of prototypes, the 'this' keyword, callback patterns, and edge case handling.

The most commonly asked polyfills are for array methods: map, filter, and reduce. Array.prototype.map creates a new array by applying a callback function to every element of the original array. The callback receives three arguments: the current element, the index, and the full array. Your polyfill must create a new array, iterate over the original, call the callback for each element, push the result, and return the new array. The filter polyfill is similar but only includes elements for which the callback returns a truthy value. The reduce polyfill is more complex - it must handle both the with-initial-value and without-initial-value cases.

Function.prototype.bind is another interview favorite. bind creates a new function that, when called, has its 'this' keyword set to a specific value, with optional preset arguments (partial application). The key challenge in implementing bind is handling both regular function calls and constructor calls (when the bound function is used with 'new'). A complete polyfill checks whether the function is being called as a constructor and adjusts the 'this' binding accordingly.

Promise is the most complex polyfill to write but demonstrates deep understanding of asynchronous JavaScript. A simplified Promise implementation requires: a constructor that takes an executor function, internal state management (pending/fulfilled/rejected), a then method that registers callbacks and returns a new Promise for chaining, and a catch method. The key insight is that then callbacks must be called asynchronously (via setTimeout or queueMicrotask) even if the promise is already resolved, and that then must return a new Promise to enable chaining.

Object.create creates a new object with the specified prototype. Before ES5, this was the primary way to set up inheritance. The polyfill creates a temporary constructor function, sets its prototype to the desired object, and returns a new instance. Array.prototype.flat recursively flattens nested arrays to a specified depth. The polyfill must handle the depth parameter (defaulting to 1) and use recursion or a stack-based approach to flatten nested arrays.

When writing polyfills in an interview, always start by defining the method on the prototype (e.g., Array.prototype.myMap), handle edge cases (empty arrays, missing callbacks, sparse arrays), and explain the time and space complexity. Show that you understand not just the happy path but the complete contract of each method.

Code examples

Polyfill for Array.prototype.map

Array.prototype.myMap = function(callback, thisArg) {
  if (typeof callback !== 'function') {
    throw new TypeError(callback + ' is not a function');
  }

  const result = [];
  for (let i = 0; i < this.length; i++) {
    // Skip holes in sparse arrays (like native map)
    if (i in this) {
      result[i] = callback.call(thisArg, this[i], i, this);
    }
  }
  return result;
};

// Test
const nums = [1, 2, 3, 4, 5];
const doubled = nums.myMap(x => x * 2);
console.log(doubled);

// With index and array arguments
const indexed = nums.myMap((val, idx) => `${idx}:${val}`);
console.log(indexed);

// With thisArg
const multiplier = { factor: 10 };
const scaled = nums.myMap(function(x) {
  return x * this.factor;
}, multiplier);
console.log(scaled);

// Sparse array handling
const sparse = [1, , 3]; // hole at index 1
console.log(sparse.myMap(x => x * 2));

The polyfill iterates over the array, calls the callback with (element, index, array), and collects results. Using 'i in this' skips holes in sparse arrays, matching native behavior. callback.call(thisArg, ...) ensures the correct 'this' context.

Polyfill for Array.prototype.filter and reduce

// Filter polyfill
Array.prototype.myFilter = function(callback, thisArg) {
  if (typeof callback !== 'function') {
    throw new TypeError(callback + ' is not a function');
  }

  const result = [];
  for (let i = 0; i < this.length; i++) {
    if (i in this && callback.call(thisArg, this[i], i, this)) {
      result.push(this[i]);
    }
  }
  return result;
};

console.log([1, 2, 3, 4, 5].myFilter(x => x > 3));
console.log(['hello', '', 'world', ''].myFilter(Boolean));

// Reduce polyfill
Array.prototype.myReduce = function(callback, initialValue) {
  if (typeof callback !== 'function') {
    throw new TypeError(callback + ' is not a function');
  }

  let accumulator;
  let startIndex;

  if (arguments.length >= 2) {
    // Initial value provided
    accumulator = initialValue;
    startIndex = 0;
  } else {
    // No initial value - use first element
    if (this.length === 0) {
      throw new TypeError('Reduce of empty array with no initial value');
    }
    accumulator = this[0];
    startIndex = 1;
  }

  for (let i = startIndex; i < this.length; i++) {
    if (i in this) {
      accumulator = callback(accumulator, this[i], i, this);
    }
  }

  return accumulator;
};

console.log([1, 2, 3, 4].myReduce((sum, x) => sum + x, 0));
console.log([1, 2, 3, 4].myReduce((sum, x) => sum + x));
console.log([[1, 2], [3, 4], [5]].myReduce((flat, arr) => flat.concat(arr), []));

Filter only pushes elements where the callback returns truthy. Reduce handles two cases: with initialValue (start from index 0) and without (use first element as initial, start from index 1). Empty array with no initialValue throws TypeError, matching native behavior.

Polyfill for Function.prototype.bind

Function.prototype.myBind = function(thisArg, ...boundArgs) {
  if (typeof this !== 'function') {
    throw new TypeError('Bind must be called on a function');
  }

  const originalFn = this;

  const boundFn = function(...callArgs) {
    // Check if called with 'new' - if so, 'this' should be the new instance
    const isNew = this instanceof boundFn;
    return originalFn.apply(
      isNew ? this : thisArg,
      [...boundArgs, ...callArgs]
    );
  };

  // Maintain prototype chain for 'new' usage
  if (originalFn.prototype) {
    boundFn.prototype = Object.create(originalFn.prototype);
  }

  return boundFn;
};

// Test 1: Basic binding
const user = { name: 'Alice' };
function greet(greeting, punctuation) {
  return `${greeting}, ${this.name}${punctuation}`;
}

const boundGreet = greet.myBind(user, 'Hello');
console.log(boundGreet('!'));
console.log(boundGreet('?'));

// Test 2: Partial application
function multiply(a, b) {
  return a * b;
}
const double = multiply.myBind(null, 2);
console.log(double(5));
console.log(double(10));

// Test 3: Works with 'new'
function Person(name, age) {
  this.name = name;
  this.age = age;
}
const BoundPerson = Person.myBind(null, 'Alice');
const p = new BoundPerson(25);
console.log(p.name, p.age);
console.log(p instanceof Person);

myBind returns a new function with 'this' permanently set to thisArg and optional preset arguments. The 'instanceof' check handles constructor calls where 'this' should refer to the new instance, not the bound thisArg. The prototype chain is maintained for proper instanceof behavior.

Polyfill for Promise (simplified)

class MyPromise {
  #state = 'pending';
  #value = undefined;
  #handlers = [];

  constructor(executor) {
    const resolve = (value) => {
      if (this.#state !== 'pending') return;
      this.#state = 'fulfilled';
      this.#value = value;
      this.#handlers.forEach(h => h.onFulfilled(value));
    };

    const reject = (reason) => {
      if (this.#state !== 'pending') return;
      this.#state = 'rejected';
      this.#value = reason;
      this.#handlers.forEach(h => h.onRejected(reason));
    };

    try {
      executor(resolve, reject);
    } catch (err) {
      reject(err);
    }
  }

  then(onFulfilled, onRejected) {
    return new MyPromise((resolve, reject) => {
      const handle = () => {
        try {
          if (this.#state === 'fulfilled') {
            const result = onFulfilled ? onFulfilled(this.#value) : this.#value;
            resolve(result);
          } else if (this.#state === 'rejected') {
            if (onRejected) {
              const result = onRejected(this.#value);
              resolve(result);
            } else {
              reject(this.#value);
            }
          }
        } catch (err) {
          reject(err);
        }
      };

      if (this.#state === 'pending') {
        this.#handlers.push({
          onFulfilled: () => queueMicrotask(handle),
          onRejected: () => queueMicrotask(handle)
        });
      } else {
        queueMicrotask(handle);
      }
    });
  }

  catch(onRejected) {
    return this.then(null, onRejected);
  }

  static resolve(value) {
    return new MyPromise(resolve => resolve(value));
  }

  static reject(reason) {
    return new MyPromise((_, reject) => reject(reason));
  }
}

// Test
const p = new MyPromise((resolve) => {
  setTimeout(() => resolve(42), 100);
});

p.then(val => {
  console.log('Resolved:', val);
  return val * 2;
}).then(val => {
  console.log('Chained:', val);
});

This simplified Promise polyfill demonstrates the core concepts: state management (pending/fulfilled/rejected), handler registration, asynchronous resolution via queueMicrotask, and chaining through then() returning a new Promise.

Polyfill for Object.create and Array.prototype.flat

// Object.create polyfill
Object.myCreate = function(proto, propertiesObject) {
  if (typeof proto !== 'object' && typeof proto !== 'function') {
    throw new TypeError('Object prototype may only be an Object or null');
  }

  function F() {}
  F.prototype = proto;
  const obj = new F();

  if (propertiesObject !== undefined) {
    Object.defineProperties(obj, propertiesObject);
  }

  return obj;
};

const animal = { speak() { return `${this.name} says ${this.sound}`; } };
const dog = Object.myCreate(animal);
dog.name = 'Rex';
dog.sound = 'Woof';
console.log(dog.speak());
console.log(Object.getPrototypeOf(dog) === animal);

// Array.prototype.flat polyfill
Array.prototype.myFlat = function(depth = 1) {
  const result = [];

  function flatten(arr, currentDepth) {
    for (let i = 0; i < arr.length; i++) {
      if (i in arr) {
        if (Array.isArray(arr[i]) && currentDepth < depth) {
          flatten(arr[i], currentDepth + 1);
        } else {
          result.push(arr[i]);
        }
      }
    }
  }

  flatten(this, 0);
  return result;
};

// Test flat
console.log([1, [2, 3], [4, [5]]].myFlat());     // depth 1
console.log([1, [2, [3, [4]]]].myFlat(2));        // depth 2
console.log([1, [2, [3, [4, [5]]]]].myFlat(Infinity)); // fully flat
console.log([[1, 2], , [3, 4]].myFlat());         // handles sparse

Object.create creates a new object with the specified prototype by using a temporary constructor function. Array.flat recursively flattens nested arrays up to the specified depth. Using Infinity as depth flattens completely.

Key points

Concepts covered

Polyfill, Array.prototype.map, Array.prototype.filter, Array.prototype.reduce, Function.prototype.bind, Promise, Object.create, Array.prototype.flat