Rapid-Fire JavaScript.
The first ten minutes of a frontend interview are rarely about algorithms. They're a volley of short questions — does the candidate actually understand the language? Here are the ones that come up again and again, each with an answer tight enough to say out loud in under a minute.
What this guide covers
- Types & equality —
==,null,NaN, floating point - Scope & closures — hoisting, the TDZ,
varvslet - Functions &
this— arrows, binding, currying - Async & the event loop —
setTimeoutgotchas, micro vs macrotasks - Objects & arrays — copies, prototypes, the method pairs people confuse
The questions about what a value really is
Every one of these probes the same thing: do you know how JavaScript coerces, compares, and stores values? Answer the "why", not just the "what".
== vs === — what's the difference?=== (strict) compares value and type with no coercion; == (loose) coerces operands to a common type first. 0 == "0" is true, 0 === "0" is false. Rule of thumb: always use === unless you specifically want x == null to catch both null and undefined.null vs undefined?undefined means "this was never assigned" — the engine's default for missing variables, params, and properties. null means "intentionally empty" — a value you assign on purpose. Quirk: typeof undefined === "undefined" but typeof null === "object" (a famous 1995 bug that can never be fixed). They're loosely equal (null == undefined) but not strictly (null === undefined is false).NaN === NaN false?NaN is the only value in JavaScript not equal to itself, per the IEEE 754 spec — "not a number" represents an indeterminate result, so two of them aren't necessarily the same thing. Test for it with Number.isNaN(x) (never x === NaN). Also note typeof NaN === "number".0.1 + 0.2 !== 0.3?0.1 and 0.2 have no exact binary representation, so the sum is 0.30000000000000004. Compare with a tolerance: Math.abs(a - b) < Number.EPSILON, or work in integer cents for money.false, 0, -0, 0n (BigInt zero), "", null, undefined, and NaN. Everything else is truthy — including "0", "false", [], and {}. Empty array and empty object being truthy trips people up constantly.string, number, boolean, null, undefined, symbol, bigint) are copied; objects/arrays/functions copy the pointer, so a function can mutate the caller's object but can't reassign the caller's variable.Where a variable lives, and what it remembers
Closures and hoisting are the two concepts interviewers use to separate people who've memorised syntax from people who understand the runtime.
counter() factory whose count can't be touched from outside.var is hoisted and initialised to undefined; function declarations are hoisted whole (callable before their line); let/const are hoisted but not initialised — they sit in the Temporal Dead Zone and throw a ReferenceError if touched early.var vs let vs const?var is function-scoped and hoisted to undefined; let/const are block-scoped with a TDZ. const forbids reassignment of the binding but does not freeze the object it points to — you can still obj.x = 1. Default to const, reach for let when you must reassign, avoid var.The classic for loop + setTimeout trap
A loop with var i that schedules setTimeout(() => console.log(i), 0) logs the final value of i N times — because all closures share one function-scoped i, and the callbacks run after the loop has finished.
for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); // 3, 3, 3 }
The fix is one keyword: let creates a fresh binding per iteration.
for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); // 0, 1, 2 }
this
The keyword that depends on how you call it
this is the single most misunderstood word in the language. The answer is always "it depends on the call site" — except for arrow functions.
this determined?new binds this to the new object; (2) explicit call/apply/bind sets it; (3) a method call obj.fn() binds it to obj; (4) otherwise it's the global object (or undefined in strict mode). Arrow functions ignore all of this and inherit this lexically.this, arguments, or prototype, and can't be used with new. They capture this from the enclosing scope — ideal for callbacks inside a method, wrong for object methods or prototype methods where you want dynamic this.call vs apply vs bind?this. call(thisArg, a, b) invokes immediately with comma args; apply(thisArg, [a, b]) invokes immediately with an array; bind(thisArg, ...) returns a new function with this permanently fixed, to call later. Mnemonic: Apply = Array.f(a, b, c) into f(a)(b)(c) — a chain of unary functions, each returning the next, closing over the earlier args. Useful for partial application and building specialised functions from general ones, e.g. add(5) as a pre-loaded adder.Why setTimeout(fn, 0) doesn't run now
JavaScript is single-threaded. Everything async funnels through the event loop, and the order things run in is a favourite interview puzzle.
setTimeout(fn, 0) actually do?fn immediately. It queues fn as a macrotask to run after the current synchronous code finishes and the call stack empties. It's a way to "yield" and let the browser paint or process other work before continuing.setTimeout gotchas?this inside a non-arrow timeout callback is the global object, not your instance. (4) Background/inactive tabs throttle timers to ~1s to save battery.queueMicrotask, MutationObserver) before taking one macrotask (setTimeout, setInterval, I/O, events). So a resolved Promise's .then always runs before a setTimeout(…, 0) queued at the same time..then/.catch, compose with Promise.all/race/allSettled, and separate success from error paths. Callbacks have none of that structure and suffer from inversion of control.async/await vs .then?await pauses the async function until the Promise settles, letting you write asynchronous code that reads top-to-bottom with normal try/catch. It doesn't block the thread — it yields control back to the event loop while waiting.The pairs people mix up under pressure
These are the "name the difference" questions — short, but a wrong answer signals you've been copy-pasting from Stack Overflow.
{...obj}, Object.assign, arr.slice()) duplicates the top level but shares nested references — mutating a nested object affects both. A deep copy clones everything; use structuredClone(obj) (built-in) or, for plain JSON-safe data, JSON.parse(JSON.stringify(obj)) (loses functions, undefined, and dates).map vs forEach?map returns a new array of transformed values and is chainable; forEach returns undefined and is only for side effects. Use map when you want a result, forEach when you just want to do something per element. Neither can break early — use a for…of loop for that.slice vs splice?slice(start, end) returns a copy of a section and does not mutate the original. splice(start, count, ...items) mutates in place — removing and/or inserting elements — and returns the removed ones. One letter, opposite behaviour on mutation.for…in vs for…of?for…in iterates enumerable keys (including inherited ones) — meant for objects, risky on arrays. for…of iterates values of any iterable (arrays, strings, Maps, Sets). For arrays, prefer for…of or entries().[[Prototype]] link (exposed as __proto__, set via Object.create or a constructor's .prototype). Property lookups walk this chain until found or it hits null. class is syntactic sugar over this — there are no real classes underneath, just prototype links.event.target to find what was clicked. It relies on event bubbling, uses less memory, and automatically handles elements added later — the standard pattern for dynamic lists.The bottom line
"Rapid-fire questions aren't testing recall — they're testing whether you understand the runtime well enough to explain the 'why' in a sentence. The themes repeat: coercion and identity (==, NaN, floating point), scope and lifetime (closures, hoisting, the TDZ), how this binds at the call site, and the single-threaded event loop that orders every async callback. Get fluent on those four and most of the volley answers itself."
Five you should be ready to follow up on
- Print the event-loop order. Be ready to trace a snippet mixing sync logs,
setTimeout(…,0), andPromise.resolve().then()and state the exact output order. - Write debounce from scratch. A closure over a
timerid withclearTimeouton each call is a near-guaranteed follow-up to the debounce-vs-throttle question. - Fix the loop without
let. Show the IIFE / extra-argument version too, to prove you understand whyletworks. - Implement
bind. Interviewers often ask you to polyfillFunction.prototype.bindusing closures andapply. - Deep clone edge cases. Explain why
JSONround-tripping drops functions,undefined,Date,Map, and circular refs — and whystructuredCloneis better.
Before you leave — how confident are you with this?
Your honest rating shapes when you'll see this again. No grades, no shame.
Comments
Loading comments…