Key Takeaways
- Closures retain access to lexical bindings, not frozen copies of their values.
- For ordinary functions, the call site determines
this; arrow functions inherit it from the surrounding scope. - In browsers, queued promise reactions and other microtasks run before the next task, including an eligible timer callback.
constprevents reassignment, not object mutation. Strict equality avoids type coercion, and ES module imports are live bindings.
For a JavaScript interview, prepare to explain scope, closures, this, asynchronous execution, prototypes, equality, and modules—and predict small programs using those rules. These JavaScript interview questions pair concise answers with runnable examples so you can demonstrate understanding rather than recite definitions.
For each example, predict the output first, then explain why it happens and how you'd avoid the underlying bug in production. Run each single-file sample separately in a modern browser console; the module example includes file-based Node.js instructions.
Scope and Hoisting Interview Questions: var, let, and const
Question: How do the three variable declarations differ?
Inside a function, var is function-scoped, while let and const are block-scoped. You can reassign var and let bindings; you can't reassign a const binding.
| Declaration | Scope inside functions | Reassignment | Initial binding state |
|---|---|---|---|
var | Function | Allowed | Initialized to undefined |
let | Block | Allowed | Uninitialized until declaration executes |
const | Block | Not allowed | Uninitialized until declaration executes |
Hoisting describes how bindings are established before execution reaches their declarations. A var binding initially contains undefined; accessing a let or const binding before initialization throws a ReferenceError. That inaccessible period is the temporal dead zone, or TDZ.
function inspectScope() {
console.log(total); // undefined
var total = 1;
{
let total = 2;
console.log(total); // 2
}
console.log(total); // 1
try {
console.log(label);
let label = "ready";
} catch (error) {
console.log(error.name); // ReferenceError
}
const state = { ready: false };
state.ready = true;
console.log(state.ready); // true
}
inspectScope();The inner total shadows the outer binding: the same name refers to a different variable inside that block. The object mutation succeeds because const protects the binding, not the object's properties. Prefer const by default and let when reassignment is intentional. For more comparisons, see JavaScript var, let, and const.
Closures: What Does a Function Remember?
Question: What is a closure, and why does it matter?
A closure is a function together with access to its surrounding lexical environment—the variables available where the function was created. That access can survive after the enclosing function returns. The function observes bindings, not a snapshot taken at creation time.
function createCounter() {
let count = 0;
return () => ++count;
}
const next = createCounter();
const independent = createCounter();
console.log(next()); // 1
console.log(next()); // 2
console.log(independent()); // 1Each call to createCounter() creates a separate count binding. Calls to the same returned function share that particular binding. This makes closures useful for encapsulating state and building callbacks.
Follow-up: Why does this loop produce three identical values?
function createReaders() {
const readers = [];
for (var i = 0; i < 3; i++) {
readers.push(() => i);
}
return readers;
}
console.log(createReaders().map(read => read()).join(","));
// 3,3,3All three functions capture the same function-scoped i, which is 3 when they're called. Replacing var with let produces 0,1,2 because each iteration receives a separate i binding. The issue is shared state, not timers or asynchronous execution.
this Binding: Why Does the Call Site Matter?
Question: How is this determined?
For an ordinary, unbound function, look at how it's called. A method call such as user.getThis() supplies user as the receiver; a standalone call supplies undefined in strict mode. Arrow functions instead inherit this from their surrounding lexical context.
"use strict";
const user = {
name: "Ada",
getThis() {
return this;
},
makeReader() {
return () => this.name;
}
};
const detached = user.getThis;
console.log(user.getThis() === user); // true
console.log(detached()); // undefined
console.log(detached.call({ name: "Lin" }).name); // Lin
const bound = detached.bind(user);
console.log(bound().name); // Ada
const readName = user.makeReader();
console.log(readName.call({ name: "Lin" })); // Adacall() and apply() supply a receiver for an invocation; bind() creates a new function with a bound receiver. They cannot replace an arrow function's inherited this. With new, an ordinary constructor starts with this bound to the new instance; returning another object can replace that instance as the result.
Event Loop Interview Questions: Microtasks vs. Macrotasks
Question: What runs first: synchronous code, promises, or timers?
The current synchronous execution finishes first. At a browser microtask checkpoint, queued microtasks run until the queue is empty, including newly added microtasks, before the next task starts. Promise reactions and queueMicrotask() callbacks use microtasks; timer callbacks run as tasks, often informally called macrotasks.
console.log("start");
setTimeout(() => console.log("timer"), 0);
new Promise(resolve => {
console.log("executor");
resolve();
}).then(() => console.log("promise"));
queueMicrotask(() => console.log("microtask"));
console.log("end");
// start
// executor
// end
// promise
// microtask
// timerThe promise executor runs synchronously during construction. Its then() callback runs later, and in this example it enters the microtask queue before the explicit queueMicrotask() callback. A zero-delay timer does not interrupt the current execution or bypass those microtasks.
Current synchronous execution
|
v
Drain microtasks, including newly queued microtasks
|
v
Browser may render; subsequent task can runAsk which runtime the question assumes. Node.js has event-loop phases and a separate process.nextTick() queue, so don't treat a browser scheduling diagram as a complete Node.js model. Compare the official Node.js scheduling explanation with our Node.js event loop walkthrough.
Promises and async/await: Results and Errors
Question: What does an async function return, and what does await pause?
A promise represents an eventual outcome and can be pending, fulfilled, or rejected. An async function always returns a promise. Its body runs synchronously until suspension at an await; awaiting pauses that function's continuation, not the entire JavaScript thread. An awaited rejection throws at the await expression and can be handled with try/catch.
// Self-contained stand-in for a promise-based data source.
async function readUser(id) {
if (id < 1) throw new Error("Invalid user ID");
return { id, name: `User ${id}` };
}
async function main() {
try {
const [first, second] = await Promise.all([
readUser(1),
readUser(2)
]);
console.log(first.name, second.name); // User 1 User 2
await readUser(0);
} catch (error) {
console.log(error.message); // Invalid user ID
}
}
main().catch(console.error);For independent asynchronous operations, start them together and aggregate their promises rather than unnecessarily waiting before starting the next operation. Promise.all() preserves input order in its result array and rejects when an input rejects; it doesn't cancel the remaining operations. Use Promise.allSettled() when you need every outcome.
Follow-up: then() returns a new promise. Returning a value fulfills that next promise with the value; throwing rejects it; returning a promise makes the chain adopt its outcome. Remember to return or await asynchronous work so its completion and errors remain connected to the caller. See our JavaScript async/await examples for more patterns.
Prototypal Inheritance: Delegation, Not Copying
Question: How does JavaScript find an inherited property?
Property lookup checks the object itself, then follows its prototype chain until it finds the property or reaches null. Inheritance delegates lookup; it doesn't copy the prototype's properties onto every object. Object.create(base) creates an object whose prototype is base.
const base = {
role: "engineer",
describe() {
return `${this.name}: ${this.role}`;
}
};
const person = Object.create(base);
person.name = "Ada";
console.log(person.describe()); // Ada: engineer
console.log(Object.hasOwn(person, "role")); // false
person.role = "lead";
console.log(base.role, person.role); // engineer leadperson --> base --> Object.prototype --> nullHere, assigning person.role creates an own property that shadows the inherited writable data property. The inherited method still receives person as this when called as person.describe(). Object.hasOwn() checks only own properties, not inherited ones.
Class syntax uses this same prototype-based inheritance model. Don't confuse a constructor's prototype property with an instance's internal prototype link. Review JavaScript classes and constructors to connect the two forms.
Equality: == vs. ===
Question: Does strict equality compare objects by their contents?
No. === compares without type coercion; == applies conversion rules when operand types differ. When both operands are objects, both operators compare identity, not structural contents. Prefer strict equality unless you intentionally need a particular coercion rule.
console.log(0 == false); // true
console.log(0 === false); // false
console.log(null == undefined); // true
console.log({ id: 1 } === { id: 1 }); // false
const shared = { id: 1 };
console.log(shared === shared); // true
console.log(NaN === NaN); // false
console.log(Object.is(NaN, NaN)); // true
console.log(0 === -0); // true
console.log(Object.is(0, -0)); // falseObject.is() differs from strict equality for NaN and signed zero: it considers NaN equal to itself and distinguishes positive from negative zero. It isn't a deep-equality function either.
Modules: Imports, Exports, and Live Bindings
Question: Is an imported variable a copied value?
No. ES module imports are read-only live bindings: the importer sees changes made by the exporting module but cannot reassign the imported binding. Modules also have their own scope, automatically use strict mode, and have undefined as top-level this.
Create these two files in the same directory. Node.js recognizes the .mjs extension as an explicit ES module marker, so this example needs no package configuration.
// counter.mjs
export let count = 0;
export function increment() {
count += 1;
}// main.mjs
import { count, increment } from "./counter.mjs";
console.log(count); // 0
increment();
console.log(count); // 1
console.log(this); // undefinednode main.mjsThe second read observes the updated exported binding. Trying to assign to count from main.mjs would throw a TypeError; calling the exported function lets the exporting module update its own binding.
Common Mistakes
- Giving output without the rule. Explain which scope, receiver, or queue determines it.
- Changing the question's environment. Ask whether the snippet runs as a browser script, an ES module, or Node.js.
- Ignoring failure paths. Show where a rejected promise is handled, not just the success log.
- Optimizing for trivia. After predicting behavior, explain how you'd make the code clearer in production.
FAQ
What is a closure in JavaScript?
A closure is a function together with access to its surrounding lexical environment. It can continue accessing those bindings after the enclosing function returns, and it observes their current values rather than frozen copies.
Does await block JavaScript execution?
Await suspends the current async function's continuation, not the entire JavaScript thread. Other eligible work can run while that function waits; marking CPU-heavy code async does not move it to another thread.
What runs first: a promise callback or setTimeout?
If a reaction to an already-fulfilled promise and a timer are scheduled during the same synchronous browser task, the promise reaction runs first. It runs as a microtask before the timer callback's task, after the current synchronous execution finishes.
Does const make an object immutable?
No. Const prevents reassignment of the variable binding, but an object's properties can still change unless another mechanism restricts them. Binding immutability and object immutability are different guarantees.
Further Reading
- MDN: Grammar and types — declarations, scope, and hoisting.
- MDN: Closures — lexical environments and captured state.
- MDN: this — receivers, arrows, and explicit binding.
- MDN: Using microtasks — tasks, checkpoints, and scheduling hazards.
- MDN: Using promises — chaining, composition, and error handling.
- MDN: Inheritance and the prototype chain — property lookup and constructor relationships.
- MDN: Equality comparisons and sameness — coercion, identity, and numeric edge cases.
- MDN: JavaScript modules — imports, exports, and module execution.
