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โ
| Topic | What 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โ
- Synchronous code runs first (call stack)
- Microtasks run next (Promise callbacks, queueMicrotask)
- 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);
});
});
}