JavaScript Interview Study Guide
Covers: Prototypes & this · Event Loop & Concurrency · Memory & Performance
On this page
JavaScript Interview Study Guide
Covers: Prototypes &
this· Event Loop & Concurrency · Memory & Performance
1. Prototypes & this
The Prototype Chain
Every object in JavaScript has a hidden link to another object called its prototype. When you access a property, JS walks up this chain until it finds it or reaches null.
instance
└── own properties: { name: "An" }
↓ not found? look up
Person.prototype
└── { greet: function, constructor: Person }
↓ not found? look up
Object.prototype
└── { hasOwnProperty, toString, ... }
↓
null
function Person(name) {
this.name = name; // own property
}
Person.prototype.greet = function() {
return "Hi, I'm " + this.name; // lives on prototype, not the instance
};
const p = new Person("An");
console.log(p.greet()); // "Hi, I'm An"
console.log(p.hasOwnProperty("greet")); // false - greet is on prototype
console.log(p.hasOwnProperty("name")); // true - name is own property
Key rule: hasOwnProperty checks only the object itself, not the chain.
this - The Core Rule
thisis determined by how a function is called, not where it's defined.
| How it's called | What this is |
|---|---|
obj.greet() | obj |
fn() (detached) | window (browser) / undefined (strict mode) |
new Person() | the new instance |
fn.call(obj) / .apply(obj) / .bind(obj) | whatever you pass in |
const obj = {
name: "An",
greet: function() { console.log(this.name); }
};
obj.greet(); // "An" - called on obj
const fn = obj.greet;
fn(); // undefined - detached, this = window
The setTimeout Trap
const timer = {
name: "An",
start: function() {
setTimeout(function() {
console.log(this.name); // ❌ this = window, logs undefined
}, 100);
}
};
3 fixes:
// ✅ 1. Arrow function (modern, preferred)
setTimeout(() => console.log(this.name), 100);
// ✅ 2. .bind(this)
setTimeout(function() { console.log(this.name); }.bind(this), 100);
// ✅ 3. Save reference (legacy, common in old codebases)
const self = this;
setTimeout(function() { console.log(self.name); }, 100);
Key term: Arrow functions lexically bind this - they capture it from the enclosing scope at definition time and never have their own this.
2. Event Loop & Concurrency
The 3 Queues
| Queue | What goes in | Priority |
|---|---|---|
| Call stack | All synchronous code | Runs first |
| Microtask queue | Promise.then(), queueMicrotask() | After stack, before macrotasks |
| Macrotask queue | setTimeout, setInterval, DOM events, I/O | One per loop tick |
Execution Order Rule
1. Run all synchronous code (call stack)
2. Drain ALL microtasks - fully (even new ones added during drain)
3. Run ONE macrotask
4. Go back to step 2
console.log("1"); // sync
setTimeout(() => console.log("2"), 0); // macrotask
Promise.resolve().then(() => console.log("3")); // microtask
console.log("4"); // sync
// Output: 1 → 4 → 3 → 2
Returning a Promise inside .then()
Promise.resolve()
.then(() => {
console.log("B");
return Promise.resolve(); // returning a Promise adds extra microtask ticks
})
.then(() => console.log("C"));
- Returning a plain value → next
.then()queued immediately - Returning a
Promise→ JS needs extra internal ticks to unwrap it →Cis delayed by 2 microtask ticks
Output with setTimeout(() => console.log("A"), 0) before: D → B → C → A
Why the UI Freezes (Single-Threaded JS)
JavaScript runs on one thread. A heavy loop on the call stack blocks everything - repaints, clicks, event handling - until it finishes.
// ❌ Blocks UI for ~2 seconds when clicked
button.onclick = function() {
for (let i = 0; i < 2_000_000_000; i++) {}
};
Fixes:
// ✅ 1. Web Worker - separate thread, UI stays responsive (best for CPU work)
const worker = new Worker("worker.js");
worker.postMessage("start");
// ✅ 2. Chunking with setTimeout - yield back to event loop between chunks
function crunchChunked(total, chunkSize) {
let i = 0;
function step() {
const end = Math.min(i + chunkSize, total);
for (; i < end; i++) {}
if (i < total) setTimeout(step, 0);
}
step();
}
// ✅ 3. requestAnimationFrame - chunking synced to screen repaints (good for visual work)
3. Memory & Performance
Garbage Collection (GC)
The browser automatically frees memory that can no longer be reached by running code.
function greet() {
const message = "Hello"; // allocated
console.log(message);
} // greet() ends → message unreachable → GC frees it
Closures keep values alive:
function makeCounter() {
let count = 0;
return function() {
count++; // count stays reachable via closure - NOT freed
return count;
};
}
const counter = makeCounter(); // count lives as long as counter does
The 4 Classic Memory Leaks
1. Forgotten event listeners
// ❌ Leak
window.addEventListener("resize", handler);
// ✅ Fix
window.removeEventListener("resize", handler);
2. Forgotten setInterval
// ❌ Leak
const id = setInterval(() => console.log(data), 1000);
// ✅ Fix
clearInterval(id);
3. Detached DOM nodes
// ❌ Leak - removed from DOM but variable still holds reference
let btn = document.querySelector("#btn");
document.body.removeChild(btn);
// ✅ Fix
btn = null;
4. Accidental global variables
// ❌ Leak - missing let/const/var → becomes window.user
function init() {
user = { name: "An" };
}
// ✅ Fix
function init() {
const user = { name: "An" };
}
React useEffect Cleanup Pattern
Every useEffect that sets up a listener, timer, or subscription must return a cleanup function:
useEffect(() => {
window.addEventListener("resize", handler);
const id = setInterval(fetchData, 5000);
return () => { // runs on unmount
window.removeEventListener("resize", handler);
clearInterval(id);
};
}, []);
Modern Cleanup: AbortController
Removes multiple listeners at once - preferred in modern code:
const controller = new AbortController();
window.addEventListener("resize", handler, { signal: controller.signal });
window.addEventListener("scroll", handler2, { signal: controller.signal });
// Clean up everything at once:
controller.abort();
How to Investigate Memory Leaks (Chrome DevTools)
- Open DevTools → Memory tab
- Take a heap snapshot
- Use the app for a few minutes (navigate around)
- Take another snapshot
- Compare - if object counts keep growing, you have a leak
Also check the Performance tab:
- 📈 Memory climbing steadily = leak
- 📊 Sawtooth pattern (up then drop) = normal GC behavior
Interview Answer: "SPA getting slower over time"
A complete senior-level answer covers all 3 parts:
Diagnose: Component unmounting without cleanup - event listeners, timers, and subscriptions added on mount but never removed, holding references through closures.
Find: Chrome DevTools Memory tab - take heap snapshots before and after navigation, compare object counts.
Fix checklist:
| Source | Fix |
|---|---|
| Event listeners | removeEventListener in useEffect cleanup |
| Timers | clearInterval / clearTimeout in cleanup |
| WebSocket / RxJS subscriptions | Unsubscribe in cleanup |
| Detached DOM refs | Set to null when done |
| Accidental globals | Always use const / let |
Quick Reference: Interview Cheatsheet
| Topic | Key phrase to say |
|---|---|
| Prototype chain | "JS walks up the chain until it finds the property or hits null" |
this rule | "Determined by how the function is called, not where it's defined" |
| Arrow functions | "Lexically bind this from the enclosing scope" |
| Event loop order | "Sync → drain all microtasks → one macrotask → repeat" |
| UI freeze | "JS is single-threaded - heavy sync work blocks the event loop" |
| Web Worker | "Moves CPU work off the main thread entirely" |
| Memory leak | "A reference that outlives its usefulness - GC thinks it's still needed" |
| React cleanup | "Return a cleanup function from useEffect to remove listeners and timers" |
