Topic 010: Polyfill in JS
Promise.all()
Creating a polyfill for Promise.all()
involves writing a function that behaves like the native Promise.all()
method, which takes an iterable of promises (or values) and returns a single promise that resolves when all the input promises have resolved, or rejects if any of the input promises reject. Here’s how you can implement such a polyfill:
if (!Promise.all) {
Promise.all = function (iterable) {
// Return a new promise
return new Promise(function (resolve, reject) {
// Check if the input is an iterable
if (!iterable || typeof iterable[Symbol.iterator] !== "function") {
return reject(new TypeError("Argument is not iterable"));
}
var results = [];
var completed = 0;
var total = 0;
var hasCalledReject = false;
// Helper function to process each promise
function processPromise(index, value) {
Promise.resolve(value)
.then(function (result) {
results[index] = result;
completed += 1;
// If all promises are resolved, resolve the returned promise
if (completed === total) {
resolve(results);
}
})
.catch(function (error) {
if (!hasCalledReject) {
hasCalledReject = true;
reject(error);
}
});
}
// Iterate over the input
var index = 0;
for (let item of iterable) {
processPromise(index, item);
index += 1;
total += 1;
}
// If the iterable was empty, resolve immediately
if (total === 0) {
resolve(results);
}
});
};
}
Check if Promise.all
already exists: The polyfill checks if Promise.all
is not defined to avoid overwriting the native implementation if it exists.
Create a new promise: The polyfill function returns a new Promise
.
Check if the input is iterable: It verifies whether the provided argument is an iterable. If it’s not, it rejects the promise with a TypeError
.
Initialize variables:
results
: An array to hold the results of the resolved promises.completed
: A counter to track the number of resolved promises.total
: The total number of promises in the iterable.hasCalledReject
: A flag to ensure the promise rejects only once if any input promise rejects.Process each promise: A helper function processPromise
is defined to handle each promise in the iterable:
Promise.resolve
.results
array and increments the completed
counter.results
array.Iterate over the input: The function iterates over the provided iterable, processing each item with processPromise
.
Handle empty iterables: If the iterable is empty (i.e., total
is 0), it resolves the returned promise immediately with an empty results
array.
Promise.all
is not available, providing a reliable implementation of the method.In JavaScript interviews, candidates might be asked to write polyfills for various methods and functionalities that are commonly used in modern JavaScript but may not be available in older environments. Here are some of the most common polyfills you might be asked to implement, along with their code examples:
Array.prototype.map
The map
method creates a new array populated with the results of calling a provided function on every element in the calling array.
if (!Array.prototype.map) {
Array.prototype.map = function (callback, thisArg) {
if (this == null) {
throw new TypeError("Array.prototype.map called on null or undefined");
}
if (typeof callback !== "function") {
throw new TypeError(callback + " is not a function");
}
var O = Object(this);
var len = O.length >>> 0;
var T = thisArg || undefined;
var A = new Array(len);
for (var k = 0; k < len; k++) {
if (k in O) {
A[k] = callback.call(T, O[k], k, O);
}
}
return A;
};
}
Array.prototype.filter
The filter
method creates a new array with all elements that pass the test implemented by the provided function.
if (!Array.prototype.filter) {
Array.prototype.filter = function (callback, thisArg) {
if (this == null) {
throw new TypeError("Array.prototype.filter called on null or undefined");
}
if (typeof callback !== "function") {
throw new TypeError(callback + " is not a function");
}
var O = Object(this);
var len = O.length >>> 0;
var T = thisArg || undefined;
var A = [];
for (var k = 0; k < len; k++) {
if (k in O) {
var kValue = O[k];
if (callback.call(T, kValue, k, O)) {
A.push(kValue);
}
}
}
return A;
};
}
Array.prototype.reduce
The reduce
method executes a reducer function (that you provide) on each element of the array, resulting in a single output value.
if (!Array.prototype.reduce) {
Array.prototype.reduce = function (callback, initialValue) {
if (this == null) {
throw new TypeError("Array.prototype.reduce called on null or undefined");
}
if (typeof callback !== "function") {
throw new TypeError(callback + " is not a function");
}
var O = Object(this);
var len = O.length >>> 0;
var k = 0;
var accumulator;
if (arguments.length >= 2) {
accumulator = initialValue;
} else {
while (k < len && !(k in O)) {
k++;
}
if (k >= len) {
throw new TypeError("Reduce of empty array with no initial value");
}
accumulator = O[k++];
}
while (k < len) {
if (k in O) {
accumulator = callback(accumulator, O[k], k, O);
}
k++;
}
return accumulator;
};
}
Function.prototype.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.
if (!Function.prototype.bind) {
Function.prototype.bind = function (oThis) {
if (typeof this !== "function") {
throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
}
var aArgs = Array.prototype.slice.call(arguments, 1);
var fToBind = this;
var fNOP = function () {};
var fBound = function () {
return fToBind.apply(
this instanceof fNOP ? this : oThis,
aArgs.concat(Array.prototype.slice.call(arguments))
);
};
if (this.prototype) {
fNOP.prototype = this.prototype;
}
fBound.prototype = new fNOP();
return fBound;
};
}
Object.create
The Object.create
method creates a new object, using an existing object as the prototype of the newly created object.
if (typeof Object.create !== "function") {
Object.create = function (prototype, propertiesObject) {
if (prototype !== Object(prototype) && prototype !== null) {
throw new TypeError("Object prototype may only be an Object or null");
}
function F() {}
F.prototype = prototype;
var obj = new F();
if (propertiesObject !== undefined) {
Object.defineProperties(obj, propertiesObject);
}
if (prototype === null) {
obj.__proto__ = null;
}
return obj;
};
}
Array.isArray
The isArray
method determines whether the passed value is an Array.
if (!Array.isArray) {
Array.isArray = function (arg) {
return Object.prototype.toString.call(arg) === "[object Array]";
};
}
String.prototype.trim
The trim
method removes whitespace from both ends of a string.
if (!String.prototype.trim) {
String.prototype.trim = function () {
return this.replace(/^\s+|\s+$/g, "");
};
}
Promise
Implementing a full polyfill for Promise
can be quite extensive, but here is a basic polyfill for Promise
:
(function (global) {
if (typeof global.Promise !== "function") {
global.Promise = function (executor) {
if (typeof executor !== "function") {
throw new TypeError("Promise resolver must be a function");
}
var state = "pending";
var value;
var handlers = [];
function resolve(result) {
if (state !== "pending") return;
state = "fulfilled";
value = result;
handlers.forEach(handle);
}
function reject(error) {
if (state !== "pending") return;
state = "rejected";
value = error;
handlers.forEach(handle);
}
function handle(handler) {
if (state === "fulfilled") {
handler.onFulfilled(value);
} else if (state === "rejected") {
handler.onRejected(value);
} else {
handlers.push(handler);
}
}
this.then = function (onFulfilled, onRejected) {
return new global.Promise(function (resolve, reject) {
handle({
onFulfilled: function (result) {
if (typeof onFulfilled === "function") {
try {
resolve(onFulfilled(result));
} catch (e) {
reject(e);
}
} else {
resolve(result);
}
},
onRejected: function (error) {
if (typeof onRejected === "function") {
try {
resolve(onRejected(error));
} catch (e) {
reject(e);
}
} else {
reject(error);
}
},
});
});
};
executor(resolve, reject);
};
}
})(this);
Array.forEach()
const arr = [1, 3, 4, 5, 5, 6];
const logger = (item, index) => {
console.log(item, index);
};
Array.prototype.forEach = function (callBack) {
for (let i = 0; i < this.length; i++) {
callBack(this[i], i);
}
};
arr.forEach(logger);
Array.flat()
const arr = [1, 2, 3, 4, 5, [6, 4, 3, 5, [1, 2, 3]]];
function myFlat(arr, depth = 1, output = []) {
if (depth <= 0) {
output.push(arr);
return output;
} else {
for (const item of arr) {
if (Array.isArray(item)) {
myFlat(item, depth - 1, output);
} else output.push(item);
}
}
return output;
}
console.log(myFlat(arr, 10));