For coding interviews, I think the biggest problem would be encountering unfamiliar questions (I guess this should be obvious) and deciding on which approach works best given the time constraints.
What I am trying to accomplish in this article is to lay out patterns that occurs in certain sets of questions, so that when we deal with its derivative questions in the future, we can at least don't have to do too much guesswork before we even start talking about solutions.
Before we start, I want to mention that this only focuses on Javascript coding questions only, not UI questions since they can be varied and difficult to discern patterns.
As I'm preparing for my upcoming interviews, here is a tentative list of different question types with concepts and possible patterns.
Promise programming
Like the title suggests, this tests our ability to do async programming, by reinventing the wheels like Promise.any
and Promise.all
so that we know how it works under the hood.
Patterns:
- In all cases you will loop through the iterables
promiseAny(iterable) // function similar to Promise.any
promiseAll(iterable) // similar to Promise.all
function promiseAll(iterable) {
// ...
iterable.forEach(async (item, index) => {
try {
const value = await item;
iterable[index] = value
// if function is promiseAny instead then resolve(value)
}
catch (err) {
reject(err)
}
})
// ....
}
Then the pseudocode would be as follows:
promiseAll(iter) or promiseAny(iter) {
for each element in `iter`, wait for it to load
if you return all promises, transform the input with results fetched from the elements
else resolve the iter as soon as it's fetched
}
Rewriting Array.prototype
methods
The goal of this part is really for us to get used to the workflow of the function.
Patterns:
- Callback functions accept at most four parameters
prev, curr, index, array
- Callbacks in
Array.prototype.filter
andArray.prototype.map
will return aBoolean
in order to transform the array calling the methods (denoted asthis
)
Array.prototype.filter = function(callbackFn) {
let copyArr = []
for (let i = 0; i < this.length; i++) {
if (!this[i]) continue;
else if (callbackFn.call(thisArg, this[i], i, this)){
copyArr.push(this[i])
}
}
return copyArr
}
- For
Array.prototype.reduce
the callback function will return a callback result based on previous callback result.
// in Array.prototype.reduce function
Array.prototype.myReduce = function(callbackFn, initVal) {
let startingIndex = ...
for (let i = startingIndex; i < this.length; i++) {
if (!this[i]) continue
result = callbackFn(result, this[i], i, this)
}
return result
}
The general pseudocode for doing such questions would be:
arrayMethod = function (prev, curr, index, array){
declare vars
for each element in this list
- for reduce update `prev` value using the callback function every time taking current `prev`value as input
- for others transform `this` array
}
Rewriting DOM APIs
Even though front end interview questions put less emphasis on data structures and algorithms, a handful of such questions still remain for us, such as rewriting getElementsByTagName
and getElementsByClassName
using DOM traversal and the likes of it.
Patterns:
- Using the function
traverse(element)
recursively for the elements and its offspring to check the condition of each element, for bothgetElementsByTagName
andgetElementsByClassName
export default function getElementsByTagName(rootElement, tagNameParam) {
const elements = [];
const tagName = tagNameParam.toUpperCase();
function traverse(element) {
if (element == null) return;
if (element.tagName === tagName) elements.push(element);
for (const child of element.children) {
traverse(child);
} // recursing through its children
}
for (const child of rootElement.children) {
traverse(child); // recursing through its root children
}
return elements;
}
function isSubset(a, b) {
return Array.from(a).every((value) => b.contains(value));
}
export default function getElementsByClassName(rootElement, classNames) {
const elements = [];
const classNamesSet = new Set(classNames.trim().split(/\s+/));
function traverse(element) {
if (element == null) {
return;
}
if (isSubset(classNamesSet, element.classList)) {
elements.push(element);
}
for (const child of element.children) {
traverse(child);
}
}
for (const child of rootElement.children) {
traverse(child);
}
return elements;
}
Hence the closest pseudocode describing the commonality between the two DOM APIs would be:
getElementsByAttr(rootElement, tagName = undefined, classNames = undefined) {
declaring empty list of elements
traverse(el) {
check if el satisfies a condition, for ex. its tag name matches or is contained by classNames
if so push it to the new el list
traverse every child of el and its descendants
}
traverse through root elements' offsprings
return updated list of elements
}
Functional programming
Coding questions would come in all forms and shapes, but they will demonstrate the same set of concepts including closures, this
keyword, using setTimeout
/setInterval
and Invoking functions via Function.prototype.apply()
/Function.prototype.call()
Patterns:
For debounce/throttle questions, you might image the test cases to look like this. Sort of like when you try to press the elevator button and after a certain time passed since you last pressed it, the door finally shuts itself. As for throttling, you use it when you want to prevent rage clicking, meaning that you could only click once every x seconds. Just like in Discord's slowmode chat.
let i = 0, j = 0;
function increment(val) {
val++;
}
const debouncedIncrement = debounce(increment(i), 100);
const throttledIncrement = throttle(increment(j), 100);
// t = 0
debouncedIncrement(); // i = 0
throttledIncrement(); // j = 1
// t = 50: i is still 0 because 100ms have not passed, throttled remains moot until t=100ms
debouncedIncrement(); // i = 0
throttledIncrement(); // j = 1
// t = 150: Because 100ms have passed since the last debouncedIncrement() at t = 50, increment was invoked and i is now 1
throttledIncrement(); // j = 2
Notice that in 2 functions, the variable timeoutId
can only be assessed and modified through debouncedIncrement
or simply debounce(increment(i), 100)()
. Closure hides variables behind a veil and prevent outside actors to play with it unless they know the keyword (namely which function) to access it.
export default function debounce(func, wait) {
var timeoutId = null;
return (...args) => {
clearTimeout(timeoutId)
timeoutId = setTimeout(() => func.call(this, ...args), wait)
}
}
export default function throttle(func, wait) {
let canBeToggledAgain = true;
return (...args) => {
if (canBeToggledAgain) {
func.apply(this, args)
canBeToggledAgain = false
setTimeout(() => canBeToggledAgain = true, wait)
}
}
}
Without further ado, here's the pseudocode for the 2 function s above
md
function throttle || debounce () {
set timeoutId or toggle boolean
return function if timeout ends ( clear timer if necessary ) or when condition to do so is right
}
Top comments (0)