⚡
Intermediate
15 min readJavaScript Interview Questions
Core JavaScript concepts, ES6+, and common interview questions
Essential JavaScript Interview Questions
Master JavaScript concepts that are frequently asked in interviews. This guide covers closures, prototypes, async/await, promises, the event loop, and ES6+ features.
1. Explain Closures with Examples
A closure is a function that has access to variables in its outer (enclosing) lexical scope, even after the outer function has returned.
// Basic closure
function outer() {
const message = 'Hello';
function inner() {
console.log(message); // Accesses outer's variable
}
return inner;
}
const myFunc = outer();
myFunc(); // "Hello" - closure maintains access to message
// Practical example: Private variables
function createCounter() {
let count = 0; // Private variable
return {
increment() {
count++;
return count;
},
decrement() {
count--;
return count;
},
getCount() {
return count;
}
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.getCount()); // 2
// count is not accessible directly
// Common pitfall with loops
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Prints: 3, 3, 3 (var is function-scoped)
// Solution 1: Use let (block-scoped)
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Prints: 0, 1, 2
// Solution 2: IIFE (Immediately Invoked Function Expression)
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(() => console.log(j), 100);
})(i);
}
// Prints: 0, 1, 2
2. Explain the Event Loop
The event loop is how JavaScript handles asynchronous operations despite being single-threaded.
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
// Output: 1, 4, 3, 2
// Explanation:
// 1. '1' - synchronous, prints immediately
// 4. '4' - synchronous, prints immediately
// 3. '3' - microtask (Promise), executes before macrotasks
// 2. '2' - macrotask (setTimeout), executes last
Event Loop Phases:
- Call Stack: Executes synchronous code
- Microtask Queue: Promises, queueMicrotask (higher priority)
- Macrotask Queue: setTimeout, setInterval, I/O (lower priority)
Process: Execute all synchronous code → Process all microtasks → Process one macrotask → Repeat
3. Promises vs Async/Await
Promises:
// Creating a Promise
const fetchData = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
const data = { id: 1, name: 'John' };
resolve(data); // Success
// reject(new Error('Failed')); // Failure
}, 1000);
});
};
// Using Promises
fetchData()
.then(data => {
console.log(data);
return fetchData(); // Chaining
})
.then(data => console.log(data))
.catch(error => console.error(error))
.finally(() => console.log('Done'));
// Promise.all - wait for all
Promise.all([
fetch('/api/user'),
fetch('/api/posts'),
fetch('/api/comments')
])
.then(([users, posts, comments]) => {
// All resolved
})
.catch(error => {
// Any rejection fails all
});
// Promise.race - first to complete
Promise.race([
fetch('/api/fast'),
fetch('/api/slow')
])
.then(result => console.log('First wins:', result));
// Promise.allSettled - wait for all (success or failure)
Promise.allSettled([
Promise.resolve(1),
Promise.reject('error'),
Promise.resolve(3)
])
.then(results => {
// [
// { status: 'fulfilled', value: 1 },
// { status: 'rejected', reason: 'error' },
// { status: 'fulfilled', value: 3 }
// ]
});
Async/Await:
// Async function always returns a Promise
async function getData() {
try {
const response = await fetch('/api/data');
const data = await response.json();
return data;
} catch (error) {
console.error('Error:', error);
throw error;
}
}
// Sequential execution
async function sequential() {
const user = await fetchUser(); // Wait
const posts = await fetchPosts(user.id); // Then wait
return { user, posts };
}
// Parallel execution
async function parallel() {
const [user, posts, comments] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchComments()
]);
return { user, posts, comments };
}
// Error handling
async function handleErrors() {
try {
const data = await fetchData();
} catch (error) {
if (error.status === 404) {
console.log('Not found');
} else {
throw error; // Re-throw
}
} finally {
console.log('Cleanup');
}
}
4. Explain Prototypes and Prototypal Inheritance
// Every object has a prototype
const obj = {};
console.log(obj.__proto__ === Object.prototype); // true
// Constructor function
function Person(name, age) {
this.name = name;
this.age = age;
}
// Methods on prototype (shared by all instances)
Person.prototype.greet = function() {
return `Hi, I'm ${this.name}`;
};
const john = new Person('John', 30);
console.log(john.greet()); // "Hi, I'm John"
console.log(john.__proto__ === Person.prototype); // true
// Inheritance
function Developer(name, age, language) {
Person.call(this, name, age); // Call parent constructor
this.language = language;
}
// Set up prototype chain
Developer.prototype = Object.create(Person.prototype);
Developer.prototype.constructor = Developer;
Developer.prototype.code = function() {
return `${this.name} codes in ${this.language}`;
};
const jane = new Developer('Jane', 25, 'JavaScript');
console.log(jane.greet()); // Inherited from Person
console.log(jane.code()); // "Jane codes in JavaScript"
// Modern ES6 classes (syntactic sugar)
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} makes a sound`;
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // Call parent constructor
this.breed = breed;
}
speak() {
return `${this.name} barks`;
}
}
const dog = new Dog('Rex', 'Labrador');
console.log(dog.speak()); // "Rex barks"
5. var vs let vs const
// var - function scoped, hoisted
console.log(x); // undefined (hoisted, but not initialized)
var x = 5;
function test() {
var y = 10;
if (true) {
var y = 20; // Same variable
}
console.log(y); // 20 - var is function scoped
}
// let - block scoped, not hoisted
// console.log(a); // ReferenceError: Cannot access before initialization
let a = 5;
if (true) {
let b = 10;
}
// console.log(b); // ReferenceError: b is not defined
// const - block scoped, cannot be reassigned
const PI = 3.14;
// PI = 3.14159; // TypeError: Assignment to constant variable
// But object properties can be modified
const obj = { name: 'John' };
obj.name = 'Jane'; // OK
obj.age = 30; // OK
// obj = {}; // TypeError: Assignment to constant variable
// Best practice
const config = Object.freeze({
API_URL: 'https://api.example.com',
TIMEOUT: 5000
});
// config.API_URL = 'new'; // Silently fails in strict mode
6. this Keyword - Different Contexts
// Global context
console.log(this); // window (browser) or global (Node.js)
// Object method
const person = {
name: 'John',
greet() {
console.log(this.name); // 'John'
}
};
person.greet();
// Losing context
const greetFunc = person.greet;
greetFunc(); // undefined (this is window/global)
// Arrow functions - lexical this
const person2 = {
name: 'Jane',
greet: function() {
setTimeout(() => {
console.log(this.name); // 'Jane' - arrow function uses parent's this
}, 100);
},
greetRegular: function() {
setTimeout(function() {
console.log(this.name); // undefined - regular function has own this
}, 100);
}
};
// call, apply, bind
function introduce(greeting, punctuation) {
return `${greeting}, I'm ${this.name}${punctuation}`;
}
const user = { name: 'Alice' };
// call - invoke immediately with arguments
console.log(introduce.call(user, 'Hello', '!')); // "Hello, I'm Alice!"
// apply - invoke immediately with array of arguments
console.log(introduce.apply(user, ['Hi', '.'])); // "Hi, I'm Alice."
// bind - return new function with bound this
const boundIntroduce = introduce.bind(user);
console.log(boundIntroduce('Hey', '?')); // "Hey, I'm Alice?"
// Class context
class Counter {
constructor() {
this.count = 0;
// Bind in constructor
this.increment = this.increment.bind(this);
}
increment() {
this.count++;
}
// Arrow function as class property
decrement = () => {
this.count--;
}
}
const counter = new Counter();
const inc = counter.increment;
inc(); // Works - bound in constructor
counter.decrement(); // Always works - arrow function
7. Array Methods - map, filter, reduce
const numbers = [1, 2, 3, 4, 5];
// map - transform each element
const doubled = numbers.map(n => n * 2);
console.log(doubled); // [2, 4, 6, 8, 10]
// filter - select elements
const evens = numbers.filter(n => n % 2 === 0);
console.log(evens); // [2, 4]
// reduce - accumulate to single value
const sum = numbers.reduce((acc, n) => acc + n, 0);
console.log(sum); // 15
// Complex example - group by property
const people = [
{ name: 'John', age: 30 },
{ name: 'Jane', age: 25 },
{ name: 'Bob', age: 30 }
];
const groupedByAge = people.reduce((acc, person) => {
const age = person.age;
if (!acc[age]) {
acc[age] = [];
}
acc[age].push(person);
return acc;
}, {});
// { 30: [{name: 'John', age: 30}, {name: 'Bob', age: 30}], 25: [{name: 'Jane', age: 25}] }
// Chaining
const result = numbers
.filter(n => n > 2)
.map(n => n * 2)
.reduce((acc, n) => acc + n, 0);
console.log(result); // 24 (3*2 + 4*2 + 5*2 = 6 + 8 + 10)
// forEach - side effects (no return value)
numbers.forEach(n => console.log(n));
// find - first matching element
const firstEven = numbers.find(n => n % 2 === 0);
console.log(firstEven); // 2
// some - at least one matches
const hasEven = numbers.some(n => n % 2 === 0);
console.log(hasEven); // true
// every - all match
const allPositive = numbers.every(n => n > 0);
console.log(allPositive); // true
8. ES6+ Features
// Destructuring
const user = { name: 'John', age: 30, city: 'NYC' };
const { name, age, country = 'USA' } = user; // with default value
const arr = [1, 2, 3, 4, 5];
const [first, second, ...rest] = arr; // rest operator
// Spread operator
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = [...arr1, ...arr2]; // [1, 2, 3, 4, 5, 6]
const obj1 = { a: 1, b: 2 };
const obj2 = { b: 3, c: 4 };
const merged = { ...obj1, ...obj2 }; // { a: 1, b: 3, c: 4 } (b is overwritten)
// Template literals
const name = 'John';
const message = `Hello, ${name}!
This is a multi-line
string.`;
// Arrow functions
const add = (a, b) => a + b;
const square = x => x * x; // single parameter, no parentheses
const log = () => console.log('Hi'); // no parameters
// Default parameters
function greet(name = 'Guest', greeting = 'Hello') {
return `${greeting}, ${name}!`;
}
// Rest parameters
function sum(...numbers) {
return numbers.reduce((acc, n) => acc + n, 0);
}
sum(1, 2, 3, 4); // 10
// Object shorthand
const name = 'John';
const age = 30;
const person = { name, age }; // { name: 'John', age: 30 }
// Computed property names
const key = 'dynamicKey';
const obj = {
[key]: 'value',
[`${key}_2`]: 'value2'
};
// Optional chaining
const user = { profile: { name: 'John' } };
console.log(user?.profile?.name); // 'John'
console.log(user?.settings?.theme); // undefined (no error)
// Nullish coalescing
const value = null;
console.log(value ?? 'default'); // 'default'
console.log(value || 'default'); // 'default'
console.log(0 ?? 'default'); // 0 (0 is not null/undefined)
console.log(0 || 'default'); // 'default' (0 is falsy)
9. Deep vs Shallow Copy
// Shallow copy - only first level is copied
const original = {
name: 'John',
address: {
city: 'NYC',
zip: '10001'
}
};
// Spread operator (shallow)
const copy1 = { ...original };
copy1.name = 'Jane'; // OK
copy1.address.city = 'LA'; // Modifies original too!
// Object.assign (shallow)
const copy2 = Object.assign({}, original);
// Array shallow copy
const arr = [1, 2, [3, 4]];
const arrCopy = [...arr];
arrCopy[2][0] = 99; // Modifies original!
// Deep copy methods
// 1. JSON (limitations: loses functions, dates, undefined, symbols)
const deepCopy1 = JSON.parse(JSON.stringify(original));
// 2. structuredClone (modern, supports most types)
const deepCopy2 = structuredClone(original);
// 3. Custom recursive function
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(item => deepClone(item));
const clonedObj = {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
clonedObj[key] = deepClone(obj[key]);
}
}
return clonedObj;
}
10. Debouncing and Throttling
// Debounce - execute after delay, reset timer on new call
function debounce(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func.apply(this, args), delay);
};
}
// Usage: Search as user types
const searchInput = document.querySelector('#search');
const debouncedSearch = debounce((query) => {
console.log('Searching for:', query);
// API call here
}, 300);
searchInput.addEventListener('input', (e) => {
debouncedSearch(e.target.value);
});
// Throttle - execute at most once per delay
function throttle(func, delay) {
let lastCall = 0;
return function(...args) {
const now = Date.now();
if (now - lastCall >= delay) {
lastCall = now;
func.apply(this, args);
}
};
}
// Usage: Scroll event
const throttledScroll = throttle(() => {
console.log('Scroll position:', window.scrollY);
}, 200);
window.addEventListener('scroll', throttledScroll);
Key Interview Takeaways:
- Closures allow functions to remember their lexical scope
- Event loop: Sync → Microtasks → Macrotasks
- Async/await is syntactic sugar over Promises
- Use let/const over var for block scoping
- Arrow functions don't have their own this
- Use map for transformation, filter for selection, reduce for accumulation
- Spread/destructuring for cleaner code
- Debounce for delayed execution, throttle for rate limiting