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: usecallback.call(thisArg, element, index, array)instead of a barecallback(...), handle sparse arrays withi in this, avoid mutating the original, and return a fresh array. Five details, every one of them graded.The
bindpolyfill is harder — it needs to handlenew, 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 fullMyPromiseclass,myFlat, andmyAssign. 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
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, andObject.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.flatto 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 justcallback(...) - 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
applyto forward both bound and call-time args - Handling
newcalls —this instanceof boundFunctioncheck - 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)
Common Mistakes
- Calling the callback with
callback(element)instead ofcallback.call(thisArg, element, index, this). You drop thethisArgcontract AND the index/array arguments — both are scored against you. - Writing a
bindpolyfill that ignores thenewcase. The nativebindmakesnew (fn.bind(ctx))()use the fresh instance asthis, notctx. Thethis instanceof boundFunctioncheck is the telltale interviewer signal. - Resolving a Promise polyfill synchronously. Native
.thencallbacks always fire asynchronously — even for already-settled promises. ForgettingqueueMicrotask(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), checki in thisfor 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.includesfor IE11. A transpiler converts modern syntax to older syntax at build time. Example: Babel convertingconsttovar. 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 boundthiscontext is ignored. Instead, the newly created object becomesthis. The polyfill detects this withthis instanceof boundFunction. This is whynew (func.bind(obj))()creates a new instance rather than usingobjasthis.
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,
.thencallbacks must not fire synchronously.queueMicrotaskensures 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.