Top 50 JavaScript Interview Questions for Mid-Level Developers
Preparing for a mid-level JavaScript interview requires more than surface-level knowledge. Interviewers expect you to understand closures deeply, reason about the event loop, explain prototype inheritance, and write clean asynchronous code. This tutorial walks through the 50 most commonly asked questions, grouped by topic, with practical code examples and best practices.
Why This Matters
Mid-level developers are expected to move beyond syntax. You should be able to debug tricky scope issues, optimize performance, and explain why a piece of code behaves the way it does. Mastering these questions builds the mental model needed for senior-level work.
Section 1: Core JavaScript Concepts (Questions 1–10)
1. What is the difference between let, const, and var?
var is function-scoped and hoisted with an initial value of undefined. let and const are block-scoped and exist in the temporal dead zone until declared. const prevents reassignment but does not make objects immutable.
function example() {
console.log(x); // undefined (hoisted)
var x = 5;
// console.log(y); // ReferenceError (TDZ)
let y = 10;
const z = 15;
// z = 20; // TypeError
}
2. What is hoisting?
Hoisting moves declarations to the top of their scope during compilation. Function declarations are fully hoisted; var variables are hoisted with undefined; let and const are hoisted but uninitialized.
hoisted(); // works
function hoisted() {
console.log("I am hoisted");
}
console.log(a); // undefined
var a = 1;
3. What is the temporal dead zone?
The period between entering a scope and the actual declaration line where let and const variables cannot be accessed. Accessing them throws a ReferenceError.
4. Explain type coercion in JavaScript.
JavaScript automatically converts types in certain operations, often leading to surprising results. Use strict equality (===) to avoid implicit coercion.
console.log(1 + "2"); // "12"
console.log("5" - 2); // 3
console.log([] + []); // ""
console.log([] + {}); // "[object Object]"
console.log(true + 1); // 2
console.log(null == 0); // false
console.log(null == undefined); // true
5. What is the difference between == and ===?
== performs type coercion before comparison; === checks both type and value without conversion. Always prefer === except when intentionally checking for null or undefined together.
6. What are primitive vs reference types?
Primitives (string, number, boolean, null, undefined, symbol, bigint) are copied by value. Reference types (objects, arrays, functions) are copied by reference.
let a = 5;
let b = a;
b = 10;
console.log(a); // 5
let obj1 = { x: 1 };
let obj2 = obj1;
obj2.x = 99;
console.log(obj1.x); // 99
7. What is typeof and what are its quirks?
typeof returns a string indicating the type. Notable quirks: typeof null === "object" (a historical bug) and typeof function(){} === "function".
console.log(typeof null); // "object"
console.log(typeof undefined); // "undefined"
console.log typeof NaN); // "number"
console.log(typeof []); // "object"
8. How do you check if a value is an array?
Array.isArray([1, 2, 3]); // true
Array.isArray("not array"); // false
9. What is NaN and how do you check for it?
NaN represents a failed numeric operation. It is the only value not equal to itself. Use Number.isNaN() rather than the global isNaN(), which coerces its argument.
console.log(NaN === NaN); // false
console.log(Number.isNaN(NaN)); // true
console.log(isNaN("hello")); // true (coerced)
console.log(Number.isNaN("hello")); // false
10. What is the difference between null and undefined?
undefined means a variable has been declared but not assigned a value, or a function returns nothing. null is an intentional absence of value, assigned explicitly by the developer.
Section 2: Functions & Closures (Questions 11–20)
11. What is a closure?
A closure is a function that retains access to variables from its lexical scope even after the outer function has returned. Closures enable data privacy and function factories.
function createCounter() {
let count = 0;
return function () {
count++;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
12. What is the difference between a function declaration and a function expression?
Function declarations are hoisted entirely, so they can be called before definition. Function expressions are not hoisted in the same way — only the variable binding is hoisted.
sayHi(); // works
function sayHi() {
console.log("hi");
}
// greet(); // TypeError
const greet = function () {
console.log("hello");
};
13. What is an arrow function and how does it differ from a regular function?
Arrow functions have a concise syntax and lexically bind this. They cannot be used as constructors and have no arguments object.
const obj = {
name: "Alice",
regular: function () {
return function () {
console.log(this.name); // undefined (or window.name)
};
},
arrow: function () {
return () => console.log(this.name); // "Alice"
}
};
14. What is this in JavaScript?
this refers to the execution context. Its value depends on how a function is called: method invocation (the object), simple invocation (undefined in strict mode, global otherwise), constructor (the new instance), and explicit binding via call, apply, or bind.
const user = {
name: "Bob",
greet() { console.log(this.name); }
};
user.greet(); // "Bob"
const fn = user.greet;
fn(); // undefined (strict mode)
15. Explain call, apply, and bind.
All three set the this value of a function. call takes arguments individually, apply takes an array, and bind returns a new function with a permanently bound this.
function introduce(greeting, punctuation) {
console.log(`${greeting}, ${this.name}${punctuation}`);
}
const person = { name: "Carol" };
introduce.call(person, "Hi", "!");
introduce.apply(person, ["Hello", "."]);
const bound = introduce.bind(person, "Hey");
bound("?");
16. What is an IIFE and why use it?
An Immediately Invoked Function Expression runs as soon as it is defined. It creates a private scope, avoiding global namespace pollution.
(function () {
const privateVar = "hidden";
console.log(privateVar);
})();
17. What are higher-order functions?
Functions that accept other functions as arguments or return functions. Examples include map, filter, reduce, and custom function composers.
const compose = (f, g) => x => f(g(x));
const addOne = x => x + 1;
const double = x => x * 2;
const addThenDouble = compose(double, addOne);
console.log(addThenDouble(3)); // 8
18. What is currying?
Currying transforms a function that takes multiple arguments into a sequence of functions each taking a single argument.
const multiply = a => b => c => a * b * c;
console.log(multiply(2)(3)(4)); // 24
19. What is the difference between parameters and arguments?
Parameters are the variables listed in the function definition. Arguments are the actual values passed when the function is invoked.
20. What are default and rest parameters?
function greet(name = "Guest", ...hobbies) {
console.log(`Hi ${name}, hobbies: ${hobbies.join(", ")}`);
}
greet("Dan", "coding", "chess");
Section 3: Objects & Prototypes (Questions 21–28)
21. What is the prototype chain?
Every object has a hidden [[Prototype]] link to another object. When a property is accessed, JavaScript walks up the chain until it finds the property or reaches null.
const animal = { eats: true };
const dog = Object.create(animal);
dog.barks = true;
console.log(dog.eats); // true (from prototype)
22. What is the difference between __proto__ and prototype?
prototype is a property on constructor functions used to build the prototype of instances. __proto__ is a getter/setter on every object pointing to its actual prototype. Prefer Object.getPrototypeOf() over __proto__.
23. How does prototypal inheritance differ from classical inheritance?
JavaScript uses prototypal inheritance — objects inherit directly from other objects. Classical inheritance (as in Java) uses classes that define blueprints. ES6 class syntax is syntactic sugar over prototypes.
24. How do you create an object without a prototype?
const pure = Object.create(null);
console.log(Object.getPrototypeOf(pure)); // null
25. What is Object.create vs new?
Object.create(proto) creates an object with the specified prototype. The new keyword invokes a constructor function, creating an object whose prototype is Constructor.prototype and binding this to it.
26. What are getters and setters?
const account = {
_balance: 0,
get balance() { return this._balance; },
set balance(val) {
if (val < 0) throw new Error("Negative balance");
this._balance = val;
}
};
account.balance = 100;
console.log(account.balance); // 100
27. How do you deep clone an object?
// Simple approach (no functions, Dates, etc.)
const clone = JSON.parse(JSON.stringify(original));
// Structured clone (modern)
const clone2 = structuredClone(original);
// Custom recursive for full control
function deepClone(obj, seen = new WeakMap()) {
if (obj === null || typeof obj !== "object") return obj;
if (seen.has(obj)) return seen.get(obj);
const copy = Array.isArray(obj) ? [] : {};
seen.set(obj, copy);
for (const key of Reflect.ownKeys(obj)) {
copy[key] = deepClone(obj[key], seen);
}
return copy;
}
28. What is the difference between Object.keys, Object.values, and Object.entries?
They return arrays of an object's own enumerable string-keyed properties: keys only, values only, and [key, value] pairs respectively.
Section 4: Asynchronous JavaScript (Questions 29–38)
29. What is the event loop?
The event loop is the mechanism that lets JavaScript perform non-blocking operations despite being single-threaded. It continuously checks the call stack and the task queues (macrotasks and microtasks), moving queued callbacks to the stack when it is empty.
30. What is the difference between microtasks and macrotasks?
Microtasks (Promise callbacks, queueMicrotask, MutationObserver) run after the current task and before the next macrotask. Macrotasks (setTimeout, setInterval, I/O, UI events) are scheduled by the event loop separately. Microtasks always drain fully before a macrotask runs.
console.log("start");
setTimeout(() => console.log("timeout"), 0);
Promise.resolve().then(() => console.log("promise"));
console.log("end");
// Output: start, end, promise, timeout
31. What is a Promise?
A Promise represents the eventual result of an asynchronous operation. It has three states: pending, fulfilled, and rejected. Once settled, it cannot change state.
const p = new Promise((resolve, reject) => {
setTimeout(() => resolve("done"), 100);
});
p.then(val => console.log(val)).catch(err => console.error(err));
32. What is async/await?
Syntactic sugar over Promises that lets you write asynchronous code that looks synchronous. await pauses the function until the Promise settles. Errors are handled with try/catch.
async function fetchUser(id) {
try {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error("Not found");
return await res.json();
} catch (err) {
console.error(err);
}
}
33. What is Promise.all vs Promise.allSettled vs Promise.race vs Promise.any?
Promise.all: rejects if any input rejects; resolves with all values.Promise.allSettled: waits for all, resolves with status/value for each.Promise.race: settles with the first Promise to settle (resolve or reject).Promise.any: resolves with the first fulfilled value; rejects only if all reject.
34. How do you run async operations in parallel with a concurrency limit?
async function mapWithConcurrency(items, limit, asyncFn) {
const results = [];
const executing = new Set();
for (const item of items) {
const p = Promise.resolve().then(() => asyncFn(item));
results.push(p);
executing.add(p);
p.finally(() => executing.delete(p));
if (executing.size >= limit) {
await Promise.race(executing);
}
}
return Promise.all(results);
}
35. What is callback hell and how do you avoid it?
Deeply nested callbacks make code hard to read and maintain. Avoid it with Promises, async/await, modular functions, and early returns.
36. What does setTimeout(fn, 0) do?
It schedules fn as a macrotask to run after the current execution stack and any pending microtasks clear. It does not run immediately.
37. How do you cancel a Promise?
Promises are not cancellable natively. Use AbortController with fetch, or wrap with a race against a rejection.
const controller = new AbortController();
fetch("/api", { signal: controller.signal })
.then(r => r.json())
.catch(err => console.error(err.name));
controller.abort(); // AbortError
38. What is the difference between setTimeout and setInterval?
setTimeout runs a callback once after a delay. setInterval runs it repeatedly with a fixed delay between calls. Both return IDs usable with clearTimeout and clearInterval.
Section 5: ES6+ Features (Questions 39–44)
39. What is destructuring?
const { name, age = 30 } = user;
const [first, , third] = [1, 2, 3];
function getUser({ name }) { return name; }
40. What is the spread and rest operator?
Both use .... Spread expands an iterable into individual elements; rest collects multiple elements into an array.
const merged = [...arr1, ...arr2];
const { a, ...rest } = obj;
function sum(...nums) { return nums.reduce((x, y) => x + y, 0); }
41. What are template literals?
Backtick strings that support interpolation, multiline text, and tagged templates.
const name = "Eve";
const html = `Hello ${name.toUpperCase()}`;
42. What are Symbols and why use them?
Symbols are unique, immutable primitives often used as object keys to avoid collisions and to define well-known behaviors (e.g., Symbol.iterator).
const id = Symbol("id");
const obj = { [id]: 123, name: "Frank" };
console.log(Object.keys(obj)); // ["name"] — Symbol keys are hidden
43. What are Map and Set?
Map holds key-value pairs with any key type and preserves insertion order. Set stores unique values. Both have size, forEach, and iteration methods.
const map = new Map([["a", 1], ["b", 2]]);
map.set("c", 3);
const set = new Set([1, 2, 2, 3]); // {1, 2, 3}
44. What are generators?
Functions that can pause and resume execution using yield. They return an iterator and are useful for lazy sequences and async flows.
function* counter() {
let i = 0;
while (true) yield i++;
}
const gen = counter();
console.log(gen.next().value); // 0
console.log(gen.next().value); // 1
Section 6: DOM, Events & Browser (Questions 45–47)
45. What is event delegation?
Attaching a single listener to a parent element rather than many listeners to children. It leverages event bubbling and works for dynamically added elements.
document.querySelector("#list").addEventListener("click", e => {
if (e.target.matches("li")) {
console.log("Clicked:", e.target.textContent);
}
});
46. What is the difference between event bubbling and capturing?
Bubbling propagates from the target up to ancestors. Capturing (trickling) propagates from ancestors down to the target. The third argument to addEventListener controls this; stopPropagation halts further propagation.
47. What is the difference between localStorage, sessionStorage, and cookies?
localStorage: persists until cleared, ~5MB, string-only.sessionStorage: cleared when the tab closes, ~5MB.- Cookies: sent with HTTP requests, ~4KB, configurable expiry and domain.
Section 7: Performance & Best Practices (Questions 48–50)
48. How do you debounce and throttle a function?
Debouncing delays execution until a pause in calls. Throttling limits execution to at most once per interval.
function debounce(fn, delay) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
function throttle(fn, limit) {
let inThrottle = false;
return function (...args) {
if (inThrottle) return;
fn.apply(this, args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
};
}
49. What is a memory leak and how do you prevent it?
A memory leak occurs when memory that is no longer needed is not released. Common causes include forgotten timers, lingering event listeners, detached DOM references, and closures holding large data. Prevent them by clearing intervals, removing listeners, using WeakMap/WeakSet where appropriate, and avoiding global variables.
50. What are best practices for writing maintainable JavaScript?
- Prefer
constby default; useletonly when reassignment is needed. - Use strict equality (
===) and explicit type checks. - Keep functions small, pure, and single-purpose.
- Handle errors explicitly with
try/catchand validation. - Use descriptive names and consistent formatting (Prettier, ESLint).
- Modularize code with ES modules and avoid deep nesting.
- Write tests for critical logic and document public APIs.
- Avoid premature optimization; profile before refactoring for speed.
Conclusion
Mastering these 50 questions gives you a strong foundation for any mid-level JavaScript interview. The key is not memorizing answers but understanding the underlying mechanics — how scope, the event loop, prototypes, and asynchronous patterns interact. Practice by writing small experiments for each concept, explain your reasoning out loud, and connect theory to real bugs you have debugged. With this preparation, you will be able to reason confidently about any JavaScript problem an interviewer throws your way.