Skip to main content

JavaScript Interview Guide

JavaScript-specific questions appear in most front-end and full-stack interviews. Interviewers want to know that you understand not just the syntax, but how JavaScript works under the hood.

The Most Common JS Interview Topicsโ€‹

TopicWhat Interviewers Really Ask
Closures"What will this code print?" (scope trap)
Event Loop"What order do these async operations run?"
this keyword"What is this in this context?"
Prototypes"How does inheritance work in JS?"
Promises/Async"What's the difference between these patterns?"
Types & Equality"What does == vs === do?"
Array Methods"Implement map/filter/reduce"
ES6+ Features"Explain destructuring, spread, generators"

Closuresโ€‹

A closure is a function that retains access to its lexical scope even when executed outside that scope.

Classic Closure Trapโ€‹

// What does this print?
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// Output: 3, 3, 3
// Why? var is function-scoped. All callbacks share the same `i`.
// By the time they run, i = 3.

// Fix 1: Use let (block-scoped)
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// Output: 0, 1, 2 โœ“

// Fix 2: Use IIFE to capture the value
for (var i = 0; i < 3; i++) {
((j) => setTimeout(() => console.log(j), 0))(i);
}
// Output: 0, 1, 2 โœ“

Practical Closure: Function Factoryโ€‹

function makeCounter(start = 0) {
let count = start; // Closed over by the returned functions

return {
increment: () => ++count,
decrement: () => --count,
getCount: () => count,
reset: () => { count = start; }
};
}

const counter = makeCounter(10);
counter.increment(); // 11
counter.increment(); // 12
counter.getCount(); // 12
// `count` is private โ€” can't be accessed from outside!

Closure Interview Question: Memoizeโ€‹

function memoize(fn) {
const cache = new Map();

return function(...args) {
const key = JSON.stringify(args);

if (cache.has(key)) {
console.log('cache hit');
return cache.get(key);
}

const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}

const memoFib = memoize(function fib(n) {
if (n <= 1) return n;
return memoFib(n - 1) + memoFib(n - 2);
});

memoFib(40); // Computed (fast, not exponential)
memoFib(40); // cache hit โ†’ instant

The Event Loopโ€‹

JavaScript is single-threaded, but handles async operations through the event loop.

Execution Order Rulesโ€‹

  1. Synchronous code runs first (call stack)
  2. Microtasks run next (Promise callbacks, queueMicrotask)
  3. Macrotasks run after all microtasks are done (setTimeout, setInterval, I/O)
console.log('1');

setTimeout(() => console.log('2'), 0); // Macrotask

Promise.resolve().then(() => console.log('3')); // Microtask

console.log('4');

// Output: 1, 4, 3, 2
// Why?
// 1. "1" (sync)
// 4. "4" (sync)
// 3. "3" (microtask โ€” runs before macrotasks)
// 2. "2" (macrotask โ€” setTimeout, even with 0ms delay)

Tricky Event Loop Exampleโ€‹

console.log('start');

setTimeout(() => {
console.log('timeout 1');
Promise.resolve().then(() => console.log('promise inside timeout'));
}, 0);

Promise.resolve()
.then(() => {
console.log('promise 1');
return Promise.resolve();
})
.then(() => console.log('promise 2'));

setTimeout(() => console.log('timeout 2'), 0);

console.log('end');

// Output:
// start
// end
// promise 1
// promise 2
// timeout 1
// promise inside timeout
// timeout 2

this Keywordโ€‹

this refers to the object that is calling the function. Its value depends on how the function is called.

// 1. Regular function call โ†’ `this` is global (or undefined in strict mode)
function greet() {
console.log(this);
}
greet(); // window (or undefined in strict mode)

// 2. Method call โ†’ `this` is the object
const obj = {
name: 'Alice',
greet() {
console.log(this.name);
}
};
obj.greet(); // 'Alice'

// 3. Arrow function โ†’ `this` is inherited from enclosing scope
const obj2 = {
name: 'Bob',
greet: () => {
console.log(this.name); // `this` is the outer scope (probably window)
}
};
obj2.greet(); // undefined (or window.name)

// 4. Explicit binding: call, apply, bind
function greet(greeting) {
console.log(`${greeting}, ${this.name}`);
}

const person = { name: 'Charlie' };
greet.call(person, 'Hello'); // "Hello, Charlie"
greet.apply(person, ['Hi']); // "Hi, Charlie"

const boundGreet = greet.bind(person);
boundGreet('Hey'); // "Hey, Charlie"

Classic this Trapโ€‹

class Timer {
constructor() {
this.seconds = 0;
}

start() {
// โŒ WRONG: `this` inside regular function callback is undefined
setInterval(function() {
this.seconds++; // TypeError!
}, 1000);

// โœ“ Fix 1: Arrow function (lexical `this`)
setInterval(() => {
this.seconds++; // Works!
}, 1000);

// โœ“ Fix 2: Bind
setInterval(function() {
this.seconds++;
}.bind(this), 1000);
}
}

Prototypes & Inheritanceโ€‹

Every JavaScript object has a prototype chain. When you access a property, JS walks up the chain.

function Animal(name) {
this.name = name;
}

Animal.prototype.speak = function() {
return `${this.name} makes a sound.`;
};

function Dog(name, breed) {
Animal.call(this, name); // Call parent constructor
this.breed = breed;
}

// Set up prototype chain
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

Dog.prototype.bark = function() {
return `${this.name} barks!`;
};

const dog = new Dog('Rex', 'Labrador');
dog.speak(); // "Rex makes a sound." (inherited from Animal.prototype)
dog.bark(); // "Rex barks!" (on Dog.prototype)

// Modern equivalent using class syntax
class AnimalES6 {
constructor(name) {
this.name = name;
}

speak() {
return `${this.name} makes a sound.`;
}
}

class DogES6 extends AnimalES6 {
constructor(name, breed) {
super(name);
this.breed = breed;
}

bark() {
return `${this.name} barks!`;
}
}

Promises & Async/Awaitโ€‹

Promise Statesโ€‹

// Promise states: pending โ†’ fulfilled or rejected

const promise = new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.5) {
resolve('Success!');
} else {
reject(new Error('Failed!'));
}
}, 1000);
});

promise
.then(value => console.log(value)) // Fulfilled
.catch(error => console.log(error)) // Rejected
.finally(() => console.log('Done')); // Always runs

Promise Combinatorsโ€‹

// Promise.all โ€” wait for ALL, fail fast on any rejection
const results = await Promise.all([
fetch('/api/user'),
fetch('/api/posts'),
fetch('/api/comments')
]);
// All 3 run in parallel, all must succeed

// Promise.allSettled โ€” wait for ALL, never rejects
const results2 = await Promise.allSettled([
fetch('/api/a'),
fetch('/api/b') // Even if this fails
]);
results2.forEach(result => {
if (result.status === 'fulfilled') console.log(result.value);
if (result.status === 'rejected') console.log(result.reason);
});

// Promise.race โ€” resolves/rejects with the FIRST to settle
const fastest = await Promise.race([
fetch('/api/server1'),
fetch('/api/server2'),
new Promise((_, reject) => setTimeout(() => reject('timeout'), 5000))
]);

// Promise.any โ€” resolves with FIRST to fulfill (ignores rejections)
const firstSuccess = await Promise.any([
fetch('/api/a'), // This one might fail
fetch('/api/b'), // This one succeeds first
]);

Async/Await Patternsโ€‹

// Sequential (one after another)
async function sequential() {
const user = await fetchUser(1); // Wait for this
const posts = await fetchPosts(user); // Then this
return posts;
// Total time: t1 + t2
}

// Parallel (both at once โ€” usually preferred)
async function parallel() {
const [user, posts] = await Promise.all([
fetchUser(1),
fetchAllPosts()
]);
return { user, posts };
// Total time: max(t1, t2)
}

// Error handling
async function withErrorHandling() {
try {
const data = await fetchData();
return data;
} catch (error) {
console.error('Failed:', error);
throw error; // Re-throw if you can't handle it
} finally {
cleanup();
}
}

// Retry pattern
async function withRetry(fn, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxRetries) throw error;
await new Promise(resolve => setTimeout(resolve, 2 ** attempt * 1000));
}
}
}

Type Coercion & Equalityโ€‹

// == performs type coercion
0 == false // true (false coerced to 0)
1 == '1' // true ('1' coerced to 1)
null == undefined // true
[] == false // true ([] โ†’ '' โ†’ 0 โ†’ false)

// === strict equality (no coercion)
0 === false // false
1 === '1' // false
null === undefined // false

// Falsy values in JavaScript:
// false, 0, -0, 0n, '', "", ``, null, undefined, NaN

// Gotchas
NaN === NaN // false! (NaN is not equal to itself)
Number.isNaN(NaN) // true (use this instead)
typeof null // 'object' (historical bug)
typeof undefined // 'undefined'
typeof [] // 'object'
Array.isArray([]) // true

Array Methods (Must Know)โ€‹

const nums = [1, 2, 3, 4, 5];

// map โ€” transform each element, returns new array
nums.map(x => x * 2) // [2, 4, 6, 8, 10]

// filter โ€” keep elements matching predicate
nums.filter(x => x % 2 === 0) // [2, 4]

// reduce โ€” accumulate values
nums.reduce((acc, x) => acc + x, 0) // 15

// find โ€” first matching element (or undefined)
nums.find(x => x > 3) // 4

// findIndex โ€” index of first match (or -1)
nums.findIndex(x => x > 3) // 3

// some โ€” true if ANY matches
nums.some(x => x > 4) // true

// every โ€” true if ALL match
nums.every(x => x > 0) // true

// flat โ€” flatten nested arrays
[[1, 2], [3, [4, 5]]].flat() // [1, 2, 3, [4, 5]]
[[1, 2], [3, [4, 5]]].flat(2) // [1, 2, 3, 4, 5]

// flatMap โ€” map + flat(1) in one pass
[1, 2, 3].flatMap(x => [x, x * 2]) // [1, 2, 2, 4, 3, 6]

Implement map/filter/reduce from scratchโ€‹

Array.prototype.myMap = function(fn) {
const result = [];
for (let i = 0; i < this.length; i++) {
result.push(fn(this[i], i, this));
}
return result;
};

Array.prototype.myFilter = function(fn) {
const result = [];
for (let i = 0; i < this.length; i++) {
if (fn(this[i], i, this)) result.push(this[i]);
}
return result;
};

Array.prototype.myReduce = function(fn, initialValue) {
let acc = initialValue !== undefined ? initialValue : this[0];
const start = initialValue !== undefined ? 0 : 1;

for (let i = start; i < this.length; i++) {
acc = fn(acc, this[i], i, this);
}

return acc;
};

Common Interview Questionsโ€‹

1. Implement debounceโ€‹

function debounce(fn, delay) {
let timer;

return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}

// Use case: search input
const search = debounce((query) => {
fetch(`/api/search?q=${query}`);
}, 300);

input.addEventListener('input', (e) => search(e.target.value));
// Only fires API call 300ms after user stops typing

2. Implement throttleโ€‹

function throttle(fn, limit) {
let lastCall = 0;

return function(...args) {
const now = Date.now();

if (now - lastCall >= limit) {
lastCall = now;
return fn.apply(this, args);
}
};
}

// Use case: scroll handler
const onScroll = throttle(() => {
updateParallax();
}, 100);

window.addEventListener('scroll', onScroll);
// Fires at most once every 100ms

3. Deep Clone an Objectโ€‹

// Simple deep clone
function deepClone(obj) {
if (obj === null || typeof obj !== 'object') return obj;
if (obj instanceof Date) return new Date(obj);
if (obj instanceof Array) return obj.map(deepClone);

const clone = {};
for (const key of Object.keys(obj)) {
clone[key] = deepClone(obj[key]);
}
return clone;
}

// Or use structuredClone (modern, handles most cases)
const clone = structuredClone(originalObj);

4. Flatten a Nested Objectโ€‹

function flattenObject(obj, prefix = '') {
return Object.keys(obj).reduce((acc, key) => {
const newKey = prefix ? `${prefix}.${key}` : key;

if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {
Object.assign(acc, flattenObject(obj[key], newKey));
} else {
acc[newKey] = obj[key];
}

return acc;
}, {});
}

flattenObject({ a: { b: { c: 1 }, d: 2 }, e: 3 });
// { 'a.b.c': 1, 'a.d': 2, 'e': 3 }

5. Implement Promise.all from scratchโ€‹

function promiseAll(promises) {
return new Promise((resolve, reject) => {
if (!promises.length) return resolve([]);

const results = new Array(promises.length);
let completed = 0;

promises.forEach((promise, index) => {
Promise.resolve(promise)
.then(value => {
results[index] = value;
completed++;

if (completed === promises.length) {
resolve(results);
}
})
.catch(reject);
});
});
}