The questions companies actually ask. The concepts that separate senior engineers from juniors. Master these — and you'll never be caught off guard in a technical interview again.
After thousands of engineering interviews at companies like Google, Meta, Netflix, and early-stage startups, a clear pattern emerges: the same 10 conceptual pillars come up again and again. Not syntax. Not library tricks. Conceptual depth — the ability to explain why JavaScript behaves the way it does, and what the trade-offs are between different approaches.
This guide covers each one with a full interactive simulation. Don't just read — play with the sim. The muscle memory of watching things break and fix in real time is worth ten times more than memorizing a blog post.
JavaScript is single-threaded — it can only do one thing at a time. Yet it handles network requests, timers, and user interactions all seemingly at once. How?
The answer is the Event Loop: a continuous cycle that checks whether the Call Stack is empty, then moves callbacks from queues into the stack. There are two queue types you must know:
queueMicrotask(). These have higher priority and are fully drained before any macrotask runs.setTimeout, setInterval, I/O events. Processed one at a time.The classic interview question: "What order do these log?"
console.log("1");
setTimeout(() => console.log("4"), 0);
Promise.resolve().then(() => console.log("3"));
console.log("2");Answer: 1 → 2 → 3 → 4. Synchronous first, then all microtasks, then macrotasks — even with a 0ms timeout.
→ 🎮 Open Event Loop Simulation — Step through the Call Stack, Web API, Microtask Queue, and Task Queue in real time.
This is one of the most impactful security decisions a frontend developer makes. The wrong choice hands attackers your users' sessions.
The three options:
| Storage | XSS Safe? | Persists? | Verdict |
|---|---|---|---|
localStorage | ❌ No | ✅ Yes | Never use for auth tokens |
sessionStorage | ❌ No | ❌ No | Still vulnerable to XSS |
| HttpOnly Cookie | ✅ Yes | ✅ Yes | Recommended for refresh tokens |
| Memory (JS var) | ✅ Yes | ❌ No | Recommended for access tokens |
Why is localStorage dangerous? Any XSS attack — even a single injected <script> tag — can do localStorage.getItem('token') and silently exfiltrate your JWT to an attacker's server.
The correct pattern: store your access token in a JavaScript variable (memory), and store your refresh token in an HttpOnly; Secure; SameSite=Strict cookie. On page load, silently call /refresh to get a new access token.
→ 🎮 Open Token Storage Simulation — Simulate XSS and CSRF attacks across all three storage strategies.
A closure is what you get when a function captures variables from its outer scope, even after that outer function has finished executing. It's one of JavaScript's most powerful — and most misunderstood — features.
function makeCounter() {
let count = 0; // lives in this closure's scope
return function() {
return ++count; // references outer 'count'
};
}
const counter1 = makeCounter();
const counter2 = makeCounter();
counter1(); // 1
counter1(); // 2
counter2(); // 1 ← completely independent!Each call to makeCounter() creates a new scope — a new "backpack" of variables that the returned function carries with it. counter1 and counter2 have their own private count. This is how closures give you private state without classes.
Real-world applications: memoization caches, React hooks (useState closes over state), debounce/throttle implementations, module patterns, and event handler factories.
→ 🎮 Open Closures Simulation — Visualize scope chains and watch closures maintain independent memory.
Before Promises, nested callbacks created the infamous "Pyramid of Doom." Promises introduced a chainable, readable way to handle async operations. async/await is syntactic sugar on top that makes async code look synchronous.
// The old way: callback hell
doA(function(a) {
doB(a, function(b) {
doC(b, function(c) { /* 😭 */ });
});
});
// The modern way: async/await
async function run() {
try {
const a = await doA();
const b = await doB(a);
const c = await doC(b);
} catch (err) {
// one catch for all errors ✅
}
}Critical pattern: Promise.all() for parallelism. Sequential awaits add up:
// ❌ Sequential: 1s + 1s + 1s = 3 seconds
const a = await fetchA();
const b = await fetchB();
const c = await fetchC();
// ✅ Parallel: all run at once = 1 second
const [a, b, c] = await Promise.all([fetchA(), fetchB(), fetchC()]);→ 🎮 Open Promises Simulation — Watch a 5-step fetch pipeline execute, handle errors, and see Promise.all() race in parallel.
Search inputs, scroll handlers, and resize events can fire hundreds of times per second. Without control, you're hammering your API or killing performance.
Debounce — "Wait for silence": Only fires after N milliseconds of inactivity. Perfect for search inputs — waits until the user stops typing.
Throttle — "Rate limit": Fires at most once every N milliseconds no matter how often the event triggers. Perfect for scroll handlers — gives you regular updates without flooding.
// Debounce: API call fires only after 500ms of no typing
const debouncedSearch = debounce((query) => searchAPI(query), 500);
// Throttle: scroll handler fires at most every 200ms
const throttledScroll = throttle(() => updateScrollPosition(), 200);The difference: if you type 10 characters in 400ms, debounce fires once (after you stop). Throttle fires on the first character, then again every 200ms thereafter.
→ 🎮 Open Debounce & Throttle Simulation — Type in the live input box and watch raw events vs debounced vs throttled fire counts in real time.
Unlike class-based languages (Java, C++), JavaScript uses prototype chains for inheritance. Every object has a hidden [[Prototype]] link pointing to another object (its prototype). Property lookups walk this chain until they find the property — or hit null.
const dog = { name: "Buddy" };
// dog → Dog.prototype → Animal.prototype → Object.prototype → null
dog.toString(); // not on dog, not on Dog.prototype... found on Object.prototype!When you use ES6 class syntax, JavaScript still uses prototypes under the hood — the class syntax is just sugar. class Dog extends Animal sets up Dog.prototype.__proto__ = Animal.prototype.
Key interview question: "What's the difference between __proto__ and prototype?"
prototype is a property on constructor functions — it's what instances will link to.__proto__ is the actual [[Prototype]] link on an instance.→ 🎮 Open Prototypal Inheritance Simulation — Click properties and watch JavaScript walk the chain step by step to find them.
Direct DOM manipulation is expensive — browsers reflow, repaint, and recalculate styles on every change. The Virtual DOM is a lightweight JavaScript object representation of the real DOM tree.
On every state change, React:
This process is called the diffing algorithm (O(n) complexity with React's heuristics). React assumes elements of the same type will produce similar trees, and uses key props to efficiently match list items.
Without Virtual DOM, updating a list of 1000 items might mean replacing all 1000 DOM nodes. With reconciliation, adding one new item at the end means exactly 1 DOM insertion.
→ 🎮 Open Virtual DOM Simulation — Compare old vs new virtual trees, watch the diff operations queue build up, and count how many real DOM mutations are needed.
JavaScript's single-threaded nature means heavy computation blocks the UI. If fibonacci(40) takes 2 seconds on the main thread, your app freezes for 2 seconds — no animations, no clicks, nothing.
Web Workers run in a completely separate OS thread. They can't touch the DOM, but they can perform heavy computation and communicate with the main thread via postMessage().
// main.js
const worker = new Worker('worker.js');
worker.postMessage({ n: 40 }); // send task
worker.onmessage = (e) => console.log(e.data); // receive result
// worker.js
onmessage = (e) => {
const result = fibonacci(e.data.n); // runs in background
postMessage(result); // send back to main
};While the worker computes, the main thread stays at 60fps. Users can still click buttons, animations still run, inputs still respond.
Use workers for: large JSON parsing, image processing, cryptographic operations, WebAssembly execution, and any operation that takes >16ms.
→ 🎮 Open Web Workers Simulation — Compare UI freeze (blocking) vs smooth (worker) side by side with live FPS counter.
Every time you see "Sign in with Google" or "Continue with GitHub," you're seeing OAuth 2.0's Authorization Code Flow in action. Understanding this is essential for any developer building authenticated apps.
The 7-step flow:
client_id, redirect_uri, scopeclient_secret for tokens (server-to-server, never expose secret to frontend!)access_token + refresh_tokenAuthorization: Bearer <token> headerWhy the two-step code → token exchange? Direct token in URL could leak via browser history or referrer headers. The code is useless without the client_secret.
→ 🎮 Open OAuth 2.0 Simulation — Step through all 7 actors and messages, with JWT anatomy explained.
CSP is an HTTP response header that tells the browser which sources of content are allowed to load on your page. It's a critical defense-in-depth layer against XSS attacks.
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-abc123';
connect-src 'self' https://api.yourapp.com;
frame-ancestors 'none';What each directive does:
default-src 'self' — fallback: only load resources from your own originscript-src 'nonce-xxx' — only execute scripts with the matching nonce attribute (blocks all inline XSS!)connect-src — restricts where fetch() and XHR can send data (blocks data exfiltration)frame-ancestors 'none' — prevents your page from being embedded in iframes (blocks clickjacking)The nonce pattern is powerful: your server generates a random nonce per request, adds it to trusted <script> tags, and puts it in the CSP header. Even if an attacker injects <script>, their script has no nonce and the browser refuses to run it.
→ 🎮 Open CSP Simulation — Toggle directives and test 4 real attack types (XSS, external scripts, data exfiltration, clickjacking). See which policies block which attacks.
| Concept | One-line answer |
|---|---|
| Event Loop | Sync → Microtasks (Promises) → Macrotasks (setTimeout) |
| Token Storage | Access token in memory, refresh token in HttpOnly cookie |
| Closure | Inner function retaining access to outer scope after execution |
| Promise.all() | Fire N promises in parallel, await all results |
| Debounce vs Throttle | Wait for silence vs fire at most once per interval |
| Prototype chain | Property lookup walks __proto__ links until null |
| Virtual DOM | JS diff → minimum DOM ops → batch apply |
| Web Workers | Separate thread via postMessage, no DOM access |
| OAuth Auth Code Flow | code (frontend) → tokens (backend exchange) |
| CSP | HTTP header whitelisting content sources to block XSS/clickjacking |
These 10 concepts are the foundation. From here, explore:
The simulations in this post aren't just demos — they're tools. Revisit them when a concept gets fuzzy. Build your mental model through interaction, not memorization.