Field Guide · Vol. 10
Ask → Answer → Move on

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

  1. Types & equality — ==, null, NaN, floating point
  2. Scope & closures — hoisting, the TDZ, var vs let
  3. Functions & this — arrows, binding, currying
  4. Async & the event loop — setTimeout gotchas, micro vs macrotasks
  5. Objects & arrays — copies, prototypes, the method pairs people confuse
PT.1 · Types & Equality

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).
Why is 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".
Why does 0.1 + 0.2 !== 0.3?
JavaScript numbers are IEEE 754 doubles (binary floating point). 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.
What are the falsy values?
Exactly eight: 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.
Primitive vs reference types — how are they passed?
JavaScript is always pass-by-value, but for objects the "value" is a reference. Primitives (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.
PT.2 · Scope & Closures

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.

What is a closure?
A function bundled with references to its surrounding lexical scope. The inner function keeps those variables alive after the outer function returns. It's the basis of private state, memoization, and currying — e.g. a counter() factory whose count can't be touched from outside.
What is hoisting?
Declarations are processed before any code runs. 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
}
PT.3 · Functions & 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.

How is this determined?
By the call site, in priority order: (1) 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.
Arrow function vs regular function?
Arrows have no own 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?
All set 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.
What is currying?
Transforming 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.
Debounce vs throttle?
Both limit how often a function fires. Debounce waits until activity stops for N ms, then runs once — good for search-as-you-type and resize-end. Throttle runs at most once every N ms during continuous activity — good for scroll and mousemove. Debounce = "wait for quiet"; throttle = "steady rate".
PT.4 · Async & the Event Loop

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.

What does setTimeout(fn, 0) actually do?
It does not run 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.
Other setTimeout gotchas?
(1) The delay is a minimum, not a guarantee — a busy main thread delays it. (2) Nested timeouts are clamped to ~4ms minimum after 5 levels. (3) The 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.
Microtask vs macrotask — what runs first?
After each synchronous run, the loop drains the entire microtask queue (Promise callbacks, 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.
Promise vs callback?
Promises represent a future value with three states (pending → fulfilled/rejected, settled once and immutably). They flatten "callback hell" into chainable .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?
Same machinery, nicer syntax. 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.
PT.5 · Objects & Arrays

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.

Shallow vs deep copy?
A shallow copy ({...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().
What is prototypal inheritance?
Every object has an internal [[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.
What is event delegation?
Attach one listener to a common ancestor instead of many on each child, and use 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

  1. Print the event-loop order. Be ready to trace a snippet mixing sync logs, setTimeout(…,0), and Promise.resolve().then() and state the exact output order.
  2. Write debounce from scratch. A closure over a timer id with clearTimeout on each call is a near-guaranteed follow-up to the debounce-vs-throttle question.
  3. Fix the loop without let. Show the IIFE / extra-argument version too, to prove you understand why let works.
  4. Implement bind. Interviewers often ask you to polyfill Function.prototype.bind using closures and apply.
  5. Deep clone edge cases. Explain why JSON round-tripping drops functions, undefined, Date, Map, and circular refs — and why structuredClone is better.
Rapid-fire JavaScript · Frontend Field Guides

Before you leave — how confident are you with this?

Your honest rating shapes when you'll see this again. No grades, no shame.

Comments

to join the discussion.

Loading comments…

Keep reading