Read alongside
MDN JavaScript Guide and reference: Closures
Language semantics: closures first, then the this, prototype chain, equality, number encoding and Using promises pages listed under this resource.
Objectives
- Explain closures and predict variable capture with var versus let in loops and callbacks.
- Determine this for method calls, plain calls, detached callbacks, arrow functions, bind and new.
- Explain prototype-based property lookup and how class syntax maps onto it.
- Compare ==, === and Object.is, and explain number pitfalls: floating-point error, the safe-integer range, NaN, bitwise truncation and BigInt.
- Predict promise and async/await ordering, and choose between sequential awaits, Promise.all, allSettled, race and any with correct error handling.
- Use structural typing, narrowing (typeof, in, discriminated unions, type predicates) and generics, and explain why unknown is safer than any.
- Explain type erasure and validate untrusted data such as JSON responses and form input at runtime boundaries.
Why it matters
Many frontend bugs come from language semantics rather than frameworks: a handler that loses this, a stale closure, a 64-bit ID rounded during JSON parsing, a rejected promise nobody awaited. TypeScript prevents many of them, but only inside the compiler's view of the program: data from the network, storage or user input arrives untyped whatever the annotations claim. Interviews test both halves: predicting what the runtime does, and choosing types that make invalid states hard to represent.
Core ideas
Closures. A function keeps access to the variables in scope where it was created, not to snapshots of their values. let and const create a fresh binding per loop iteration; var is function-scoped, so callbacks created in a var loop all see the final value. Closures implement private state, memoization and debouncing, and they cause stale-value bugs when a callback holds on to an old variable.
this. For ordinary functions the call decides this: obj.f() binds obj; a plain call f() gets undefined in strict code (modules and class bodies are strict); new binds the newly created object; call, apply and bind set it explicitly. Arrow functions have no this of their own and use the enclosing one. Passing obj.method as a callback detaches it from obj.
Prototypes. Property lookup walks the prototype chain until it finds the key or reaches null. class syntax builds the same structure: methods live on Class.prototype and are shared by all instances, with stricter rules such as requiring new.
Equality and numbers. === compares without type conversion; == coerces, so null == undefined, '' == 0 and [] == false are all true. Object.is differs from === only for NaN (equal to itself) and for +0 versus -0. Objects compare by reference. Every number is an IEEE 754 double: 0.1 + 0.2 !== 0.3, integers are exact only up to Number.MAX_SAFE_INTEGER (2^53 − 1), and bitwise operators convert their operands to 32-bit integers. Send 64-bit IDs as strings and use BigInt when exact large integers matter.
Promises and async/await. A promise settles once. Callbacks registered with then, and the code after an await, always run later as microtasks, even when the promise has already settled. An async function always returns a promise, and a throw inside it becomes a rejection. Sequential awaits in a loop serialize independent work; Promise.all runs it concurrently but rejects on the first failure while the other operations keep running; allSettled collects every outcome; race settles with the first promise to settle and any fulfills with the first to fulfill. array.forEach(async ...) waits for nothing.
TypeScript's model. Compatibility is structural: a value fits a type if it has the required members, whatever its declared name. Fresh object literals additionally get excess-property checks. Narrowing refines a union through control flow with typeof, instanceof, in, equality checks, discriminant fields and user-defined type predicates; assigning the leftover case to never makes a switch exhaustive. any turns checking off and spreads silently; unknown forces you to narrow before use. Generics express relationships between inputs and outputs, and constraints such as T extends { id: string } state what the body may assume.
Erasure. The compiler removes types: as assertions and type parameters perform no runtime checks. JSON.parse and response.json() return any, so validate external data where it enters, with a schema library or hand-written type guards, and keep trusted types inside.
type Result = { ok: true; value: number } | { ok: false; error: string };
function parse(raw: unknown): Result {
if (typeof raw === 'object' && raw !== null && 'value' in raw && typeof raw.value === 'number') {
return { ok: true, value: raw.value };
}
return { ok: false, error: 'invalid payload' };
}
Common misconceptions
- "Arrow functions are just shorter functions." They have no own
thisorargumentsand cannot be called withnew, which is why they suit callbacks inside methods and fail as object methods that needthis. - "
awaitpauses the program." It suspends only the current async function; other code, including further event handlers, runs meanwhile. - "
Promise.allcancels the rest when one fails." Promises cannot be cancelled; the remaining work continues and its results are ignored. Use anAbortControllerto actually stop requests. - "TypeScript validates API responses." Types vanish at runtime;
as Useron unvalidated JSON is an unchecked claim. - "
constmakes a value immutable." It prevents rebinding the name; the object itself can still be mutated.
How to approach it
Predict-then-run small snippets in the browser console or Node for each idea. Graduate interviews tend to check closures, this, equality and async ordering; mid-level and senior frontend interviews add typing an API client, modeling UI state with discriminated unions and explaining what the type system can and cannot guarantee. Implementing debounce, memoize, a promise pool with a concurrency limit and a typed event emitter covers most language-level practice.
Self-check before you finish
- Why do callbacks created in a for loop with var all see the same value?
- What is this inside a class method passed directly to setTimeout?
- Where does a class method live, and how is it found from an instance?
- How does === differ from Object.is?
- Why is 2 ** 53 + 1 === 2 ** 53 true?
- What happens to the other operations when Promise.all rejects?
- Why is unknown safer than any for the result of JSON.parse?
- What does an as assertion check at runtime?