Most Asked Polyfills in JavaScript Interviews
What are Polyfills?
Polyfills are pieces of code (usually JavaScript) that provide functionality on older browsers that do not natively support certain features. Essentially, polyfills allow developers to use modern web features and APIs while ensuring compatibility with older environments. They act as a bridge between newer and older versions of web technologies.
Why are Polyfills Needed?
- Browser Compatibility: Different browsers and different versions of the same browser may not support all the latest web standards. Polyfills help ensure that all users have a consistent experience regardless of the browser they use.
- Future-Proofing: As web standards evolve, using polyfills allows developers to write code that conforms to modern practices, knowing that it will still work in older environments.
- Development Convenience: Polyfills enable developers to use new features and syntax without waiting for full support across all browsers.
Topics Covered:
- Function.prototype.call
- Function.prototype.apply
- Function.prototype.bind
- Array.prototype.map
- Array.prototype.filter
- Array.prototype.reduce
- Array.prototype.forEach
- Debounce
- Throttle
- Memoize
- Promise.all
In JavaScript interviews, it’s common to discuss polyfills, which are code snippets that replicate the functionality of newer browser features for older browsers. Let’s explore some frequently asked polyfills and their implementations:
1. Call
The call
method calls a function with a given this
value and arguments provided individually.
Object.prototype.myCall = function(callObj, ...params) {
if (typeof this !== "function") {
throw new Error(this + " is not a Function");
}
callObj.tempFunction = this;
const result = callObj.tempFunction(...params);
delete callObj.tempFunction;
return result;
};
let object1 = {
name: "Vivek",
surname: "moradiya",
printName: function(age) {
return this.name + " " + this.surname + " " + age;
}
};
let object2 = {
name: "Amy",
surname: "Patel"
};
console.log(object1.printName.myCall(object2, 22)); // Amy Patel 22
Explanation: This polyfill temporarily assigns the function to the callObj
object, calls it with the provided arguments, and then removes the temporary function to avoid polluting the object.
2. Apply
The apply
method calls a function with a given this
value and arguments provided as an array.
Object.prototype.myApply = function(applyObj, params) {
if (typeof this !== "function") {
throw new Error(this + " is not a Function");
}
applyObj.tempFunction = this;
const result = applyObj.tempFunction(...params);
delete applyObj.tempFunction;
return result;
};
let object1 = {
name: "Vivek",
surname: "moradiya",
printName: function(age, city) {
return this.name + " " + this.surname + " " + age + " " + city;
}
};
let object2 = {
name: "Amy",
surname: "Patel"
};
console.log(object1.printName.myApply(object2, [22, "morbi"])); // Amy Patel 22 morbi
Explanation: Similar to myCall
, but myApply
accepts an array of arguments which are spread when calling the function.
3. Bind
The bind
method creates a new function that, when called, has its this
keyword set to the provided value, with a given sequence of arguments preceding any provided when the new function is called.
Object.prototype.myBind = function(bindObj, ...params) {
if (typeof this !== "function") {
throw new Error(this + " is not a Function");
}
const self = this;
return function(...args) {
return self.apply(bindObj, [...params, ...args]);
};
};
let object1 = {
name: "Vivek",
surname: "moradiya",
printName: function(age, city) {
return this.name + " " + this.surname + " " + age + " " + city;
}
};
let object2 = {
name: "Amy",
surname: "Patel"
};
let boundFunction = object1.printName.myBind(object2, 22, "Morbi");
console.log(boundFunction()); // Amy Patel 22 Morbi
Explanation: The myBind
function returns a new function. When this new function is called, it calls the original function with the bindObj
as this
and combines the bound parameters with any new ones.
4. Map
The map
method creates a new array populated with the results of calling a provided function on every element in the calling array.
Array.prototype.myMap = function(callback, context) {
let arr = [];
for (let i = 0; i < this.length; i++) {
arr.push(callback.call(context, this[i], i, this));
}
return arr;
};
let arr = [1, 2, 4, 5, 6, 4];
let context = {
multiplier: 7,
offset: 10
};
let newArr = arr.myMap(function(value) {
return value * this.multiplier + this.offset;
}, context);
console.log(newArr); // [17, 24, 38, 45, 52, 38]
Explanation: We iterate over the array, applying the callback function to each element, and use call
to set the context. The results are stored in a new array.
5. Filter
The filter
method creates a new array with all elements that pass the test implemented by the provided function.
Array.prototype.myFilter = function(callback, context) {
let arr = [];
for (let i = 0; i < this.length; i++) {
if (callback.call(context, this[i], i, this)) {
arr.push(this[i]);
}
}
return arr;
};
let arr = [1, 2, 4, 5, 6, 4];
let context = {
condition: 5
};
let newArr = arr.myFilter(function(value) {
return value > this.condition;
}, context);
console.log(newArr); // [6]
Explanation: Similar to myMap
, but myFilter
only includes elements that pass the condition in the callback function
6. Reduce
The reduce
method executes a reducer function on each element of the array, resulting in a single output value.
Array.prototype.myReduce = function(callback, acc) {
let output = acc;
let startIndex = 0;
if (output === undefined) {
output = this[0];
startIndex++;
}
for (let i = startIndex; i < this.length; i++) {
output = callback(output, this[i], i, this);
}
return output;
};
const friends = [
{ name: "Anna", books: ["Bible", "Harry Potter"] },
{ name: "Bob", books: ["War and peace", "Romeo and Juliet"] },
{ name: "Alice", books: ["The Lord of the Rings", "The Shining"] }
];
const allBooks = friends.myReduce((acc, cur) => [...acc, ...cur.books], []);
console.log(allBooks); // ["Bible", "Harry Potter", "War and peace", "Romeo and Juliet", "The Lord of the Rings", "The Shining"]
Explanation: We iterate over the array, applying the reducer function to accumulate the results. If no initial accumulator is provided, we use the first element of the array.
7. ForEach
The forEach
method executes a provided function once for each array element.
Array.prototype.myForEach = function(callback) {
for (let i = 0; i < this.length; i++) {
callback(this[i], i, this);
}
};
const arrData = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
arrData.myForEach((element) => {
console.log(element);
});
Explanation: We iterate over the array and call the provided callback function for each element.
8. Debounce
The debounce
function ensures that a function is not called too frequently. It delays the function execution until after a certain period has elapsed since the last time it was invoked.
function debounce(fn, delay) {
let timeoutId;
return function(...args) {
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
const debouncedFunction = debounce(() => {
console.log('Debounced function called');
}, 2000);
debouncedFunction();
debouncedFunction();
debouncedFunction();
Explanation: The debounce
function sets a timeout each time it is called. If it is called again before the delay period ends, the previous timeout is cleared and a new one is set. This ensures that the function is only executed once the delay period has elapsed without another call.
9. Throttle
The throttle
function ensures that a function is called at most once every specified period.
function throttle(fn, limit) {
let lastCall = 0;
return function(...args) {
const now = Date.now();
if (now - lastCall >= limit) {
lastCall = now;
fn.apply(this, args);
}
};
}
const throttledFunction = throttle(() => {
console.log('Throttled function called');
}, 2000);
throttledFunction();
throttledFunction();
throttledFunction();
Explanation: The throttle
function checks the time elapsed since the last call. If it is greater than or equal to the limit, the function is executed and the last call time is updated.
10. Memoize
The memoize
function caches the results of function calls to improve performance for expensive computations.
function myMemoize(fn) {
const cache = {};
return function(...args) {
let argCache = JSON.stringify(args);
if (!cache[argCache]) {
cache[argCache] = fn.call(this, ...args);
}
return cache[argCache];
};
}
const expensiveFunc = (num1, num2) => {
let output = 1;
for (let i = 0; i <= 10000000; i++) {
output += i;
}
return num1 + num2 + output;
}
const memoizeFunc = myMemoize(expensiveFunc);
console.time();
console.log(memoizeFunc(1, 2));
console.timeEnd();
console.time();
console.log(memoizeFunc(1, 2));
console.timeEnd();
Explanation: The myMemoize
function uses a cache object to store the results of function calls. If the function is called again with the same arguments, the cached result is returned.
11. Promise.all
The Promise.all
method takes an iterable of promises and returns a single promise that resolves when all of the promises resolve or rejects when any of the promises reject.
const p1 = new Promise(function (resolve, reject) {
setTimeout(() => {
resolve("resolved 1");
}, 1000);
});
const p2 = new Promise(function (resolve, reject) {
setTimeout(() => {
reject("rejected 2");
}, 2000);
});
const p3 = new Promise(function (resolve, reject) {
setTimeout(() => {
resolve("resolved 3");
}, 3000);
});
const p4 = new Promise(function (resolve, reject) {
setTimeout(() => {
resolve("resolved 4");
}, 3000);
});
Promise.myAll = function (promises) {
return new Promise(function (resolve, reject) {
let result = [];
let total = 0;
promises.forEach((item, index) => {
Promise.resolve(item)
.then((res) => {
result[index] = res;
total++;
if (total === promises.length) resolve(result);
})
.catch((err) => {
reject(err);
});
});
});
};
Promise.myAll([p1, p2, p3])
.then((res) => {
console.log(res);
})
.catch((err) => {
console.log(err);
});
Promise.myAll([p1, p3, p4])
.then((res) => {
console.log(res);
})
.catch((err) => {
console.log(err);
});
Explanation: The myAll
function returns a new promise. It resolves with an array of results when all promises are resolved or rejects with the first encountered rejection.
Polyfills ensure that modern JavaScript code can run smoothly in all browsers, providing greater accessibility and functionality to users across different platforms. Understanding and implementing these polyfills demonstrates a developer’s commitment to maintaining high standards of web development.