Back to Notes

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 debounce function 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 debounce function which accepts a callback function and a wait duration. Calling debounce() 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:
  1. cancel() method to cancel pending invocations
  2. flush() 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 promisify that 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 (usually Promises) as an input, and returns a single Promise that 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.all accepts any iterable like Map, 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 a Promise object that has already been resolved as an empty array.
  • For each outcome object, a status string is present. If the status is 'fulfilled', then a value is present. If the status is 'rejected', then a reason is 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 (usually Promises). 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 an AggregateError, 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 AggregateError object 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 a Promise object 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 a Promise. 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 the then() 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 sum function 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 curry function 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 deepClone function that performs a deep clone operation on JavaScript objects. You can assume the input only contains JSON-serializable values (nullbooleannumberstringArrayObject) and will not contain any other objects like DateRegexMap or Set.
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 deepClone function 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 classNames string, 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 an HTMLCollection of Elements.
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 an HTMLCollection of Elements.
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() vs Element.style

While 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. The style property 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 the style property is empty.

Since the function is meant to match the style rendered by the browser, getComputedStyle() should be used instead of element.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:

  1. Styling can be done in many ways: There are many ways to style an element on a webpage:
    1. Inline: Directly within the HTML tag.
    2. Internal: With a <style> tag.
    3. External: Linking to a separate CSS file.
  2. 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.
  3. Multiple ways of defining styles properties: Other than using the raw final values when styling elements:
    1. Many value types: Some properties like colors can be done in different ways. e.g. color: whitecolor: #fffcolor: rgb(255, 255, 255) are all valid ways to render white text color.
    2. Shorthands: Properties can be defined using shorthands, e.g. margin: 10px results in margin-top: 10px as well as for the other directions.
    3. 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-color variable.
  4. 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 using pxremem, etc but the resolved value unit for these properties obtained from getComputedStyle() is px. 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 like inheritrem which 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
}