← All courses

Interview questionsWeb DevelopmentBeginner

Top 10 JavaScript interview questions

Closures, hoisting, this, the event loop, promises and more: the JavaScript questions that decide most frontend and full-stack screening rounds, with crisp answers and code.

10 min · 10 questions

1What is a closure? Give a practical example.

A closure is a function that remembers the variables from the scope where it was created, even after that outer function has finished running.

js
function createRateLimiter(limit) {
  let calls = 0;                 // private state, kept alive by the closure
  return function () {
    if (calls >= limit) return false;
    calls++;
    return true;
  };
}
const canCall = createRateLimiter(3);
canCall(); canCall(); canCall(); canCall(); // true true true false

Practical uses: private variables (nothing outside can change calls), function factories, memoization caches, and utilities like debounce, throttle and once. Mention the cost: closed-over variables stay in memory as long as the function is reachable.

2Explain hoisting, and the difference between var, let and const.

Before running code, JavaScript registers declarations in each scope. That's hoisting.

  • var is hoisted and initialised to undefined, so reading it early gives undefined. It is function-scoped.
  • let and const are hoisted but not initialised: reading them before the declaration throws a ReferenceError (the "temporal dead zone"). They are block-scoped.
  • const cannot be re-assigned, but an object or array held in a const can still be mutated.
  • Function declarations are fully hoisted (callable before they appear); function expressions are not.
js
sayHi();            // works
function sayHi() {}
greet();            // TypeError: greet is not a function (var greet is undefined)
var greet = () => {};

Use const by default, let when you must re-assign, and never var in new code.

3How does this work in JavaScript?

this depends on how a function is called:

  • Method call obj.fn() → obj.
  • Plain call fn() → undefined in strict mode (or the global object in sloppy mode).
  • new Fn() → the newly created object.
  • fn.call(ctx), fn.apply(ctx), fn.bind(ctx) → ctx.
  • Arrow functions have no own this; they use this from the surrounding code.
js
const btn = {
  label: "Save",
  onClickRegular() { setTimeout(function () { console.log(this.label); }, 0); }, // undefined
  onClickArrow() { setTimeout(() => console.log(this.label), 0); },             // "Save"
};

This is why arrow functions are handy for callbacks inside methods, but shouldn't be used as object methods that need this themselves.

4Explain the event loop. What does this print?

js
console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve().then(() => console.log("C"));
(async () => { console.log("D"); await null; console.log("E"); })();
console.log("F");

Answer: A D F C E B

JavaScript runs on one thread. Synchronous code runs first (A, then D, because an async function runs synchronously until its first await, then F). When the stack is empty, the event loop drains the microtask queue (promise callbacks and code after await: C, then E) before taking the next macrotask (timer callbacks: B). Rendering happens between macrotasks, which is why a long synchronous loop freezes the page.

5What are promises, and how is async/await different?

A promise represents a value that will be available later; it is pending, then fulfilled or rejected, exactly once. .then handles success, .catch handles errors, .finally runs either way.

async/await is syntax on top of promises: an async function returns a promise, and await pauses that function until a promise settles, so asynchronous code reads top to bottom and errors are handled with try/catch.

js
async function getProfile(id) {
  try {
    const res = await fetch(`/api/users/${id}`);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return await res.json();
  } catch (e) {
    report(e);
    return null;
  }
}

Key point: run independent calls in parallel with await Promise.all([...]) instead of awaiting them one by one.

6What is the difference between == and ===?

=== (strict equality) compares type and value with no conversion. == (loose equality) converts both sides to a common type first, which produces surprises:

js
"" == 0            // true
"0" == false       // true
null == undefined  // true
[] == ![]          // true

Always use ===. The one accepted exception is value == null, a short way to check for both null and undefined. Also know that NaN !== NaN, so test with Number.isNaN(x), and that Object.is treats NaN as equal to itself and +0 and -0 as different.

7What are debounce and throttle? Implement debounce.

Both limit how often a function runs during rapid events.

  • Debounce: wait until events stop for N ms, then run once. Use for search-as-you-type, auto-save, window resize end.
  • Throttle: run at most once every N ms during continuous events. Use for scroll tracking, drag, mouse move.
js
function debounce(fn, delay) {
  let timer;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

const onSearch = debounce((q) => fetchResults(q), 300);
input.addEventListener("input", (e) => onSearch(e.target.value));

The closure keeps timer between calls; each new call cancels the pending one.

8What is event delegation and event bubbling?

When an event fires on an element, it bubbles up through its ancestors, triggering their listeners too (after an initial capture phase going down).

Event delegation uses bubbling: put one listener on a parent and work out which child was clicked with event.target.

js
document.querySelector("#todo-list").addEventListener("click", (e) => {
  if (e.target.matches("button.delete")) {
    e.target.closest("li").remove();
  }
});

Benefits: one listener instead of hundreds, and it works for elements added later. stopPropagation() stops bubbling; preventDefault() stops the browser's default action. They're different things.

9What is the difference between map, forEach, filter and reduce?

  • forEach runs a function for each item and returns undefined: use it for side effects.
  • map returns a new array of the same length with each item transformed.
  • filter returns a new array with only the items that pass a test.
  • reduce folds the array into a single value (a sum, an object, a grouped map).
js
const cart = [{ price: 100, qty: 2 }, { price: 50, qty: 1 }, { price: 0, qty: 3 }];
const paid = cart.filter((i) => i.price > 0);
const lineTotals = paid.map((i) => i.price * i.qty);        // [200, 50]
const total = lineTotals.reduce((sum, t) => sum + t, 0);    // 250

None of these mutate the original array (unless your callback does). Chaining them is readable; for very large arrays a single reduce or for loop avoids creating intermediate arrays.

10What is the difference between shallow copy and deep copy?

A shallow copy duplicates the top level only; nested objects are still shared with the original. A deep copy duplicates everything.

js
const user = { name: "Asha", address: { city: "Pune" } };

const shallow = { ...user };
shallow.address.city = "Delhi";
user.address.city;       // "Delhi": shared nested object

const deep = structuredClone(user);
deep.address.city = "Mumbai";
user.address.city;       // still "Delhi"

Shallow copy: spread {...obj}, [...arr], Object.assign, slice. Deep copy: structuredClone (handles dates, Maps, Sets and cycles), or JSON.parse(JSON.stringify(obj)) for plain data only (loses dates, functions and undefined). This matters in React and Redux, where you update state immutably by copying the levels you change.

Keep going