JavaScript Function Code Examples for Interviews
Quick ref: [[Debounce]] · [[Throttle]] · [[Flatten]] · [[Promise Polyfills]] · [[Currying]] · [[Array Polyfills]] · [[Deep Clone]] · [[DOM Functions]]
Debounce & Throttle
Debounce
Part - 1
- A
debouncefunction is a higher-order function (a wrapper) that delays execution until a pause occurs in the input
function debounce(func, delay) {
let timerId;
return function(...args) {
clearTimeout(timerId);
timerId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
Part - 2
- Implement a
debouncefunction which accepts a callback function and awaitduration. Callingdebounce()returns a function which has debounced invocations of the callback function following the behavior described above. - Additionally, the debounce-ed function comes with two extra methods:
cancel()method to cancel pending invocationsflush()method to immediately invoke any delayed invocations
export default function debounce(func, wait) {
let timerId;
let funcArgs;
let funcThis;
const callFunc = () => {
func.apply(funcThis, funcArgs);
};
function debounced(...args) {
clearTimeout(timerId);
funcThis = this;
funcArgs = args;
timerId = setTimeout(() => {
timerId = null;
callFunc();
}, wait);
};
debounced.cancel = () => {
clearTimeout(timerId);
timerId = null;
}
debounced.flush = () => {
if (timerId) {
callFunc();
clearTimeout(timerId);
}
};
return debounced;
}
Throttle
- Throttling is the technique of ensuring a function is called at most once every specified amount of time.
- While Debounce waits for a pause (good for typing), Throttle ensures a steady stream of updates (good for scrolling or resizing)
function throttle(func, limit) {
let isThrottled = false;
return function(...args) {
if (isThrottled) {
return;
}
func.apply(this, args);
isThrottled = true;
setTimeOut(() => {
isThrottled = false;
},limit);
};
}
[!Info] Debounce is "Run only at the end." Throttle is "Run at the start, then wait."
JS Array Functions
Flatten
Implement a function flatten that returns a newly-created array with all sub-array elements concatenated recursively into a single level.
function flatten(value) {
const result = [];
function helper(items) {
for (let i = 0; i < items.length; i++) {
if (i in items) {
if (Array.isArray(items[i])) {
helper(items[i]);
} else {
result.push(items[i]);
}
}
}
}
helper(value);
return result;
}
Promise Async
Promisify
- Implement a function
promisifythat takes a function following the common callback-last error-first style, i.e. taking a(err, value) => ...callback as the last argument, and returns a version that returns promises.
function promisify(func) {
return function(...args) {
return new Promise((resolve, reject) => {
func.call(this, ...args, (err, value) => {
if (err) {
reject(err);
} else {
resolve(value);
}
});
});
};
}
Promise.all
Promise.all()is a method that takes an iterable of elements (usuallyPromises) as an input, and returns a singlePromisethat resolves to an array of the results of the input promises. This returned promise will resolve when all of the input's promises have resolved, or if the input iterable contains no promises. It rejects immediately upon any of the input promises rejecting or non-promises throwing an error, and will reject with this first rejection message / error.- Current Implementation assumes given iterable is array however
Promise.allaccepts any iterable likeMap,Set.
function promiseAll(iterable) {
const result = new Array(iterable.length);
let completed = 0;
return new Promise((resolve, reject) => {
if (iterable.length === 0) {
resolve([]);
}
for (let i=0; i < iterable.length; i++) {
Promise.resolve(iterable[i]).then((value) => {
result[i] = value;
completed += 1;
if (completed === iterable.length) {
resolve(result);
}
}).catch((err) => reject(err));
}
});
}
Promise.allSettled
- The
Promise.allSettled()method returns a promise that resolves after all of the given promises have either fulfilled or rejected, with an array of objects that each describes the outcome of each promise. - However, if and only if an empty iterable is passed as an argument,
Promise.allSettled()returns aPromiseobject that has already been resolved as an empty array. - For each outcome object, a
statusstring is present. If the status is'fulfilled', then avalueis present. If the status is'rejected', then areasonis present. The value (or reason) reflects what value each promise was fulfilled (or rejected) with.
function promiseAllSettled(iterable) {
const result = new Array(iterable.length);
let completed = 0
return new Promise(resolve => {
if (iterable.length === 0) {
resolve([]);
}
iterable.forEach((item, i) => {
Promise.resolve(item).then((value) => {
result[i] = { status: "fulfilled", value };
}).catch((err) => {
result[i] = { status: "rejected", reason: err };
}).finally(() => {
completed ++;
if (completed === iterable.length) {
resolve(result);
}
});
});
});
}
Promise.any
Promise.any()takes an iterable of elements (usuallyPromises). It returns a single promise that resolves as soon as any of the elements in the iterable fulfills, with the value of the fulfilled promise. If no promises in the iterable fulfill (if all of the given elements are rejected), then the returned promise is rejected with anAggregateError, a new subclass of Error that groups together individual errors.- If an empty iterable is passed, then the promise returned by this method is rejected synchronously. The rejected reason is an
AggregateErrorobject whose errors property is an empty array.
function promiseAny(iterable) {
let rejectCount = iterable.length;
const errors = new Array(rejectCount);
return new Promise((resolve, reject) => {
if (rejectCount === 0) {
reject(new AggregateError(errors));
}
iterable.forEach((item, i) => {
Promise.resolve(item).then(resolve, (reason) => {
errors[i] = reason;
rejectCount--;
if (rejectCount === 0) {
reject(new AggregateError(errors));
}
});
});
});
}
Promise.race
- The
Promise.race()method returns a promise that fullfills or rejects as soon as one of the promises in an iterable fulfills or rejects, with the value or reason from that promise. - If the iterable passed is empty, the promise returned will be forever pending.
- If the iterable contains one or more non-promise value and/or an already settled promise, then
Promise.race()will resolve to the first of these values found in the iterable.
function promiseRace(iterable) {
return new Promise((resolve, reject) => {
iterable.forEach(item => {
Promise.resolve(item)
.then(resolve, reject);
});
});
}
# alternate solution
function promiseRace(iterable) {
return new Promise((resolve, reject) => {
iterable.forEach(async(item) => {
try {
const result = await item;
resolve(result);
} catch(err) {
reject(err);
}
}
});
}
Promise.reject
- The
Promise.reject()static method returns aPromiseobject that is rejected with a given reason.
function promiseReject(reason) {
return new Promise((_, reject) => {
reject(reason);
})
}
Promise.resolve
- The
Promise.resolve()static method "resolves" a given value to aPromise. If the value is:- A native promise, return that promise.
- A non-thenable, return a promise that is already fulfilled with that value.
- A thenable,
Promise.resolve()will call thethen()method and pass a pair of resolving functions as arguments. A promise that has the same state as the thenable is returned.
function promiseResolve(value) {
if (value instanceof Promise) {
return value;
}
return new Promise(resolve => {
resolve(value);
});
}
Currying
Infinite Currying (SUM)
- Implement a
sumfunction that accepts a number and allows for repeated calling with more numbers. Calling the function without an argument will sum up all the arguments thus far and return the total.
function sum(val) {
return (val2) => {
return val2 === undefined ? val : sum(val + val2);
};
}
// alternate solution
function sum(val) {
return function (val2) {
return val2 === undefined ? val : sum(val + val2);
}
}
const addTwo = sum(2);
addTwo(3)(); // 5
addTwo(4)(); // 6
addTwo(3)(4)(); // 9
- A function that is callable indefinitely, but returns a number when used in a math context. The Goal:
sum(1)(2) == 3; // true
sum(1)(2)(3)(4) == 10; // true
function sum(val) {
let currentSum = val;
function inner(val2) {
currentSum += val2;
return inner;
}
inner.valueOf = () => {
return currentSum;
}
inner.toString = () => {
return currentSum;
}
return inner;
}
Curry
- Implement the
curryfunction which accepts a function as the only argument and returns a function that accepts single arguments and can be repeatedly called until at least the minimum number of arguments have been provided (determined by how many arguments the original function accepts). The initial function argument is then invoked with the provided arguments.
function curry(func) {
return function curried(...args) {
if (args.length >= func.length) {
return func.apply(this, args);
}
return (...newArg) => {
return curried.apply(this, args.concat(newArg));
};
};
}
Polyfills
Array.prototype.square
Array.prototype.square = function() {
const n = this.length;
const result = new Array(n);
for (let i=0; i<n; i++) {
const isDefined = Object.hasOwn(this, i);
if (isDefined) {
result[i] = this[i] * this[i];
}
}
}
Array.prototype.at
Array.prototype.myAt = function(index) {
const n = this.length;
if (index >= n || Math.abs(index) > n) return undefined;
return this[(index + n) % n];
}
Array.prototype.concat
Array.prototype.myConcat = function(...items) {
const result = [...this];
for (let item of items) {
if (Array.isArray(item)) {
result.push(...item);
} else {
result.push(item);
}
}
return result;
}
Array.prototype.map
Array.prototype.myMap = function(callback, thisArg) {
const n = this.length;
const result = new Array(n);
for(let i=0; i<n; i++) {
const isDefined = Object.hasOwn(this, i);
if (isDefined) {
result[i] = callback.call(thisArg, this[i], i, this);
}
}
return result;
}
Array.prototype.filter
Array.prototype.myFilter = function(callback, thisArg) {
const n = this.length;
const result = new Array();
for (let i=0; i<n ; i++) {
const isDefined = Object.hasOwn(this, i);
if (isDefined) {
const isValid = callback.call(thisArg, this[i], i, this);
if (isValid) {
result.push(this[i]);
}
}
}
return result;
}
Array.prototype.reduce
Array.prototype.myReduce = function(callback, initialVal) {
const n = this.length;
if (n === 0 && initialVal === undefined) {
throw new TypeError("Reduce of empty array with no initial value");
}
let result;
let i = 0;
if (initialVal !== undefined) {
result = initialVal;
} else {
let isValPresent = false;
while (!isValPresent && i < n) {
if (Object.hasOwn(this, i)) {
result = this[i];
isValPresent = true;
}
i += 1;
}
}
for (i; i<n; i++) {
const isDefined = Object.hasOwn(this, i);
if (isDefined) {
result = callback(result, this[i], i, this);
}
}
return result;
}
Deep Clone
Part I
- Implement a
deepClonefunction that performs a deep clone operation on JavaScript objects. You can assume the input only contains JSON-serializable values (null,boolean,number,string,Array,Object) and will not contain any other objects likeDate,Regex,MaporSet.
function deepClone(value) {
if (typeof value !== "object" || value === null){
return value;
}
if (Array.isArray(value)) {
return value.map((ele) => deepClone(ele));
}
return Object.fromEntries(
Object.entries(value)).map((ele) => [ele[0], deepClone(ele1)]);
)
}
Part II
- Implement a
deepClonefunction that performs a deep clone as thoroughly as possible, while also handling the following:- The input object can contain any data type.
- Handle the edge case where the input object is cyclic, i.e. the circular references should also be cloned.
function deepCloneWithCache(value, cache) {
if (typeof value != "object" || value === null || value === "function"){
return value;
}
if (cache.has(value)) {
return cache.get(value);
}
if (value instanceof Date) {
return new Date(value);
}
if (value instanceof RegExp) {
return new RegExp(value);
}
if (value instanceof Map) {
const cloneMap = new Map();
value.forEach((val, key) => {
cloneMap.set(key, deepCloneWithCache(val, cache));
});
return cloneMap;
}
if (value instanceof Set) {
const cloneSet = new Set();
value.forEach((ele) => {
cloneSet.add(deepCloneWithCache(ele, cache));
});
return cloneSet;
}
let cloned;
if (Array.isArray(value)) {
cloned = [];
} else {
cloned = Object.create(Object.getPrototypeOf(value));
}
cache.set(value, newObj);
for (const key of Reflect.ownKeys(value)) {
cloned[key] = deepCloneWithCache(value[key], cache);
}
return cloned;
}
function deepClone(value) {
return deepCloneWithCache(value, new WeakMap());
}
DOM Functions
getElementsByClassName
getElementsByClassName() is a method which exists on HTML Documents and Elements to return an HTMLCollection of descendant elements within the Document/Element which has the specified class name(s).
Let's implement our own Element.getElementsByClassName() that is similar but slightly different:
- It is a pure function which takes in an element and a
classNamesstring, a string containing one or more class names to match on, separated by whitespace. E.g.getElementsByClassName(document.body, 'foo bar'). - Similar to
Element.getElementsByClassName(), only descendants of the element argument are searched, not the element itself. - Return an array of
Elements, instead of anHTMLCollectionofElements.
function isSubset(toCheck, classList) {
return Array.from(toCheck).every((className) => classList.contains(className));
}
/**
* @param {Element} element
* @param {string} classNames
* @return {Array<Element>}
*/
export default function getElementsByClassName(element, classNames) {
const elements = [];
const classes = new Set(classNames.trim().split(/\s+/));
function helper(ele) {
if(isSubset(classes, ele.classList)) {
elements.push(ele);
}
for (const child of ele.children) {
helper(child);
}
}
for (const child of element.children) {
helper(child);
}
return elements;
}
getElementsByStyle
Implement a method getElementsByStyle() that finds DOM elements that are rendered by the browser using the specified style. It is similar to Element.getElementsByClassName() but with some differences:
- It is a pure function which takes in an element, a property string, and a value string representing the style's property/value pair to be matched on the elements descendants. E.g.
getElementsByStyle(document.body, 'font-size', '12px'). - Similar to
Element.getElementsByClassName(), only descendants of the element argument are searched, not the element itself. - Return an array of
Elements, instead of anHTMLCollectionofElements.
export default function getElementsByStyle(element, property, value) {
onst elements = [];
function helper(ele) {
const eleStyles = getComputedStyle(ele);
if (eleStyles[property] === value) {
elements.push(ele);
}
for (const child of ele.children) {
helper(child);
}
}
for (const child of element.children) {
helper(child);
}
return elements;
}
Note
[!NOTE]
Window.getComputedStyle()vsElement.styleWhile both APIs return
CSSStyleDeclarations,getComputedStyle()returns an object that represents the final resolved styles of an element after all styles have been applied, including styles from CSS files, inline styles, and browser defaults. Thestyleproperty on elements allows you to access and modify inline styles directly on the element. If an element is not styled using inline styles, the values of all the keys on thestyleproperty is empty.Since the function is meant to match the style rendered by the browser,
getComputedStyle()should be used instead ofelement.style.// Assuming a typical <body> element with no inline styles specified. console.log(document.body.style.fontSize); // '' (empty string) console.log(getComputedStyle(document.body).getPropertyValue('font-size')); // 16px;What are computed styles and why are they important?
Let's take a closer look at
Window.getComputedStyle(). According to MDN, it returns the property values after applying active stylesheets and resolving any basic computation those values may contain. Obtaining resolved styles is important because:
- Styling can be done in many ways: There are many ways to style an element on a webpage:
- Inline: Directly within the HTML tag.
- Internal: With a
<style>tag.- External: Linking to a separate CSS file.
- Styles follow cascading and inheritance rules: CSS works by cascading styles – rules from different sources combine and potentially override each other. Elements can also inherit styles from their parent elements.
- Multiple ways of defining styles properties: Other than using the raw final values when styling elements:
- Many value types: Some properties like colors can be done in different ways. e.g.
color: white,color: #fff,color: rgb(255, 255, 255)are all valid ways to render white text color.- Shorthands: Properties can be defined using shorthands, e.g.
margin: 10pxresults inmargin-top: 10pxas well as for the other directions.- CSS variables: Properties can also be written using CSS variables or more officially known as CSS custom properties, e.g.
color: var(--text-color). The final color value is not known until the browser resolves the value of the--text-colorvariable.- Styles have to be resolved: The
getComputedStyle()API gives you a snapshot of the final, calculated styles applied to an element after all the cascading and inheritance rules have been applied. This is incredibly valuable because it reflects how the element is actually rendered in the browser.The implication of using
Window.getComputedStyle()is that we can only match based on the element's resolved values. Font sizes, paddings, margins, can be defined usingpx,rem,em, etc but the resolved value unit for these properties obtained fromgetComputedStyle()ispx. Colors can be defined using named colors, HSL, RGB (and more) formats but the resolved style format for colors is RGB hexadecimal. This is a limitation that you should mention during your interviews if you have the opportunity.element.style.color = 'white'; console.log(getComputedStyle(element).getPropertyValue('color')); // 'rgb(255, 255, 255)'While it is possible to write your own conversion/resolution logic within your
getElementsByStyle()function so that the value argument is resolved before comparing against the element's resolved styles, it is only achievable for certain properties that do not rely on properties of other elements. Properties likeinherit,remwhich rely on properties of other elements due to CSS cascading cannot be matched easily and accurately.
getElementsByTagName
/**
* @param {Element} el
* @param {string} tagName
* @return {Array<Element>}
*/
export default function getElementsByTagName(el, tagNameParam) {
const elements = [];
const tagName = tagNameParam.toUpperCase();
function helper(ele) {
if (ele.tagName === tagName) {
elements.push(ele);
}
for (const child of ele.children) {
helper(child);
}
}
for (const ele of el.children) {
helper(ele);
}
return elements;
}
getElementsByTagNameHierarchy
Bottom Up Solution
/**
* @param {Document} document
* @param {string} tagNames
* @return {Array<Element>}
*/
export default function getElementsByTagNameHierarchy(document, tagNames) {
const elements = [];
const tags = tagNames.trim().split(/\s+/).map((tag) => tag.toUpperCase());
const tagsLength = tags.length - 1;
function helper(ele, tagIndex) {
if (ele.tagName === tags[tagIndex]) {
if (tagIndex === tagsLength) {
elements.push(ele)
} else {
tagIndex++;
}
}
for (const child of ele.children) {
helper(child, tagIndex);
}
}
for (const child of document.children) {
helper(child, 0);
}
return elements;
}
Top Down Approach
function getElementsByTagNameHierarchy(document, tagNames) {
const elements = [];
const tags = tagNames.trim().split(/\s+/).map((tag) => tag.toUpperCase());
const tagsLength = tags.length - 1;
const lastTag = tags[tagsLength];
tags.pop();
const lastNodes = document.get
}