JavaScript Interview Prep
Miscellaneous

Polyfills

Build the Ramp When the Stairs Don't Exist

LinkedIn Hook

"Write a polyfill for Array.prototype.map" is the single most asked interview question that has nothing to do with the rest of your job.

And it catches people out every time — because it's not about knowing map. It's about whether you can: use callback.call(thisArg, element, index, array) instead of a bare callback(...), handle sparse arrays with i in this, avoid mutating the original, and return a fresh array. Five details, every one of them graded.

The bind polyfill is harder — it needs to handle new, partial application, and prototype chains. The Promise polyfill is the advanced tier — states, microtasks, chaining, recovery.

In this lesson you'll build all of these from scratch: myMap, myBind, a full MyPromise class, myFlat, and myAssign. Every line is the kind of line interviewers are watching you write.

If "polyfill vs transpiler" makes you hesitate, or if you've never written Function.prototype.myBind — this lesson is the one.

Read the full lesson -> [link]

#JavaScript #InterviewPrep #Polyfills #Promises #Frontend #CodingInterview #WebDevelopment


Polyfills thumbnail


What You'll Learn

  • The difference between a polyfill and a transpiler (and why syntax can't be polyfilled)
  • How to write interview-grade polyfills for map, bind, Promise, flat, and Object.assign
  • The subtle details interviewers grade you on — callback.call, this instanceof, sparse arrays, prototype chain

Build the Ramp When the Stairs Don't Exist

Think of polyfills like building a ramp next to stairs. The stairs (modern APIs) work great for people who can use them (modern browsers). But some people need a ramp (older browsers). A polyfill detects that the ramp is missing and builds one using basic materials (older JavaScript features).

Polyfill vs Transpilation

  • Polyfill: A piece of code that provides a missing API on older platforms. It adds a new method or feature at runtime. Example: Adding Array.prototype.flat to a browser that doesn't have it.
  • Transpilation: Converting modern syntax to older syntax at build time. Example: Babel converting arrow functions to regular functions. You can't polyfill syntax — you have to transpile it.

Polyfill: Array.prototype.map

This is the single most asked polyfill question in interviews. Understand every line:

Array.prototype.myMap = function(callback, thisArg) {
  // 'this' refers to the array myMap is called on
  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 (just 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); // [2, 4, 6, 8, 10]

// Test with thisArg
const multiplier = { factor: 10 };
const scaled = nums.myMap(function(x) {
  return x * this.factor;
}, multiplier);
console.log(scaled); // [10, 20, 30, 40, 50]

// Test with sparse arrays
const sparse = [1, , 3]; // hole at index 1
console.log(sparse.myMap(x => x * 2)); // [2, empty, 6]

Key details interviewers look for:

  • Using callback.call(thisArg, ...) — not just callback(...)
  • Passing three arguments: element, index, original array
  • Handling sparse arrays with i in this
  • Type checking the callback
  • Returning a NEW array (not mutating)

Polyfill: Function.prototype.bind

The second most common polyfill question. bind returns a new function with a bound this and optional pre-filled arguments:

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

  const originalFn = this;

  const boundFunction = function(...callArgs) {
    // If called with 'new', use the newly created object as context
    const isNewCall = this instanceof boundFunction;

    return originalFn.apply(
      isNewCall ? this : context,
      [...boundArgs, ...callArgs]
    );
  };

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

  return boundFunction;
};

// Test basic binding
const obj = { name: "Rakibul" };

function greet(greeting, punctuation) {
  return `${greeting}, ${this.name}${punctuation}`;
}

const boundGreet = greet.myBind(obj, "Hello");
console.log(boundGreet("!"));  // "Hello, Rakibul!"
console.log(boundGreet("?")); // "Hello, Rakibul?"

// Test with partial application
function add(a, b, c) {
  return a + b + c;
}

const add5 = add.myBind(null, 5);
console.log(add5(3, 2)); // 10

const add5and3 = add.myBind(null, 5, 3);
console.log(add5and3(2)); // 10

// Test with new operator
function Person(name) {
  this.name = name;
}

const BoundPerson = Person.myBind({ ignored: true });
const person = new BoundPerson("Rakibul");
console.log(person.name);          // "Rakibul"
console.log(person instanceof Person); // true (prototype maintained)

Key details:

  • Using apply to forward both bound and call-time args
  • Handling new calls — this instanceof boundFunction check
  • Preserving the prototype chain
  • Supporting partial application

Polyfill: Basic Promise

This is the advanced polyfill question. Implementing a basic Promise from scratch demonstrates deep understanding of asynchronous patterns:

class MyPromise {
  constructor(executor) {
    this.state = "pending";   // pending | fulfilled | rejected
    this.value = undefined;
    this.handlers = [];       // queued .then/.catch callbacks

    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 (error) {
      reject(error);
    }
  }

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

          if (this.state === "rejected") {
            if (onRejected) {
              const result = onRejected(this.value);
              resolve(result); // catch handler recovers
            } else {
              reject(this.value); // propagate rejection
            }
          }
        } catch (error) {
          reject(error);
        }
      };

      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(value => {
  console.log("Resolved:", value); // Resolved: 42
  return value * 2;
}).then(value => {
  console.log("Chained:", value);  // Chained: 84
});

// Test rejection
new MyPromise((_, reject) => {
  reject("Something failed");
}).catch(error => {
  console.log("Caught:", error); // Caught: Something failed
});

// Test chaining with recovery
MyPromise.reject("error")
  .catch(e => `Recovered from: ${e}`)
  .then(v => console.log(v)); // "Recovered from: error"

Polyfill: Array.prototype.flat

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

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

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

// Test
console.log([1, [2, [3, [4]]]].myFlat());    // [1, 2, [3, [4]]]
console.log([1, [2, [3, [4]]]].myFlat(2));    // [1, 2, 3, [4]]
console.log([1, [2, [3, [4]]]].myFlat(Infinity)); // [1, 2, 3, 4]

Polyfill: Object.assign

if (typeof Object.myAssign !== "function") {
  Object.myAssign = function(target, ...sources) {
    if (target == null) {
      throw new TypeError("Cannot convert undefined or null to object");
    }

    const result = Object(target);

    sources.forEach(source => {
      if (source != null) {
        // Own enumerable properties only
        Object.keys(source).forEach(key => {
          result[key] = source[key];
        });

        // Also copy enumerable Symbol properties
        Object.getOwnPropertySymbols(source).forEach(sym => {
          if (Object.prototype.propertyIsEnumerable.call(source, sym)) {
            result[sym] = source[sym];
          }
        });
      }
    });

    return result;
  };
}

// Test
const target = { a: 1, b: 2 };
const result = Object.myAssign(target, { b: 3, c: 4 }, { d: 5 });
console.log(result);       // { a: 1, b: 3, c: 4, d: 5 }
console.log(result === target); // true (mutates target)

Polyfills visual 1


Common Mistakes

  • Calling the callback with callback(element) instead of callback.call(thisArg, element, index, this). You drop the thisArg contract AND the index/array arguments — both are scored against you.
  • Writing a bind polyfill that ignores the new case. The native bind makes new (fn.bind(ctx))() use the fresh instance as this, not ctx. The this instanceof boundFunction check is the telltale interviewer signal.
  • Resolving a Promise polyfill synchronously. Native .then callbacks always fire asynchronously — even for already-settled promises. Forgetting queueMicrotask (or equivalent) makes your polyfill subtly wrong in a way real code depends on.

Interview Questions

Q: Write a polyfill for Array.prototype.map.

See implementation above. The key points are: create a new empty array, iterate with a for loop, use callback.call(thisArg, element, index, array), check i in this for sparse arrays, and return the new array.

Q: What's the difference between a polyfill and a transpiler?

A polyfill adds a missing API at runtime using older JavaScript. Example: adding Array.prototype.includes for IE11. A transpiler converts modern syntax to older syntax at build time. Example: Babel converting const to var. You can polyfill APIs but you must transpile syntax.

Q: How does bind handle the new operator?

When a bound function is called with new, the bound this context is ignored. Instead, the newly created object becomes this. The polyfill detects this with this instanceof boundFunction. This is why new (func.bind(obj))() creates a new instance rather than using obj as this.

Q: How would you implement Array.prototype.filter as a polyfill?

Same structure as map polyfill — iterate, call the callback with (element, index, array), but only push to result if the callback returns a truthy value.

Q: In the Promise polyfill, why do we use queueMicrotask?

Promises must resolve asynchronously — even if the value is immediately available, .then callbacks must not fire synchronously. queueMicrotask ensures callbacks run after the current synchronous code but before the next macrotask, matching native Promise behavior.


Quick Reference — Cheat Sheet

POLYFILL CHECKLIST — QUICK MAP

Polyfill vs Transpile:
  polyfill    -> adds a missing API at runtime (older JS)
  transpile   -> rewrites modern syntax to older (build time)
  You cannot polyfill syntax.

Array.prototype.map:
  - callback.call(thisArg, element, index, array)
  - check (i in this) for sparse arrays
  - return a NEW array (no mutation)
  - type-check the callback

Function.prototype.bind:
  - return a new function
  - apply(context, [...boundArgs, ...callArgs])
  - handle `new`: `this instanceof boundFunction` -> use `this`
  - preserve prototype chain via Object.create

Promise:
  - states: pending -> fulfilled | rejected
  - once settled, never changes state
  - handlers queue for async resolution
  - queueMicrotask for async firing
  - .then returns a NEW Promise (chaining)
  - .catch === .then(null, onRejected)

Array.prototype.flat:
  - default depth = 1
  - recurse while currentDepth < depth
  - handle sparse arrays (i in arr)

Previous: JSON.parse / JSON.stringify -> The Edge Cases That Bite in Production Next: Immutability -> The Museum-Exhibit Mindset


This is Lesson 14.3 of the JavaScript Interview Prep Course — 14 chapters, 87 lessons.

On this page