Scope, Closures & Functions
Roughly 17% of a typical loop — and 17% of the mock exam here.
Scope: three kinds, one lookup rule
var is scoped to the nearest function or module and leaks out of blocks. let and const are scoped to the nearest block. Name resolution is lexical: the engine walks outward from where the code is written, not from who called it, until it finds a binding or reaches the global scope. That is why a closure keeps working long after its creator returned.
Hoisting is often taught badly. All three declaration forms are hoisted; they differ in initialization:
varis initialized toundefined, so reading it early yieldsundefined.letandconststay uninitialized in the temporal dead zone, so reading them early throwsReferenceError.- Function declarations are hoisted with their body, so they are callable above their own line. Function expressions and arrow functions are not — the binding exists, the value does not.
One consequence worth remembering: typeof neverDeclared is 'undefined' rather than an error, but typeof x where x is a let in its TDZ still throws.
Closures capture bindings, not values
A closure is a function plus the surrounding bindings it still reaches. Those are live bindings, which explains the interview classic:
for (var i = 0; i < 3; i++) setTimeout(() => console.log(i)); // 3 3 3
for (let i = 0; i < 3; i++) setTimeout(() => console.log(i)); // 0 1 2
With var there is one binding, already 3 by the time any callback runs. With let the spec creates a fresh binding per iteration and copies the value forward before the update expression, so each callback sees its own. Before ES2015 the fix was an IIFE taking i as a parameter.
Closures are also how JavaScript does private state: a factory returning an inner function gives every call its own scope, which is the machinery behind counters, once, memoize, and debounce. In a debounce, the pending timer id must live in the enclosing closure — declare it inside the returned function and clearTimeout cancels nothing.
The leak worth mentioning out loud
A closure keeps everything it captured reachable, and sibling closures created in the same function share one context. A long-lived event handler that captures nothing large can still pin a 50 MB array that a neighbouring closure referenced. Capture the value you need — const size = big.length — and let the big object go out of scope. This is the answer that separates a memorized definition from real experience.
Parameters, defaults, and arity
Defaults are evaluated per call and only when the argument is undefined; f(null) keeps null. Parameters initialize left to right, so a default may reference an earlier parameter but not a later one — function f(a = b, b = 1) throws from the TDZ.
When any parameter has a default, the parameter list gets its own scope, separate from the body. That is why this returns 1, not 99:
function f(cb = () => x, x = 1) { var x = 99; return cb(); }
fn.length counts parameters before the first default or rest parameter — the arity libraries inspect to decide how to call a callback. Rest parameters beat arguments: they are a real array and they work in arrow functions, which have no arguments of their own.
call, apply, bind — and the stack
call and apply invoke immediately and differ only in how arguments are passed; bind returns a new function with this fixed permanently, plus any pre-supplied arguments. A bound function ignores a later call, with new as the one exception.
Two hard details worth having ready: a duplicate let in one block is a SyntaxError caught at parse time, not a runtime error; and V8 has never implemented proper tail calls, so deep recursion throws RangeError: Maximum call stack size exceeded even in tail position. Rewrite as a loop, an accumulator, or a trampoline.
Sample questions
6 of the 34 questions this domain carries in practice mode — expand one to check yourself before drilling.
1. The scoping difference between var and let is:
- var is scoped to the nearest function or module, while let is scoped to the nearest block
- var is global everywhere and let is local everywhere, regardless of the surrounding code
- var is scoped to the file it appears in, while let is visible only inside the current statement
- both are block-scoped, but only var may be reassigned after it has been initialized once
Answer: A. A var declared inside an if block leaks to the whole function. That single difference is why let and const replaced var in modern code.
2. A closure is best described as:
- any function that has already finished running but whose return value is still referenced somewhere else in the program
- a function stored on an object so it can read that object's properties through the this keyword
- a copy of the outer scope that the engine snapshots at the moment the inner function is defined
- a function together with the surrounding variable bindings it still has access to after that outer scope returns
Answer: D. Closures hold live bindings, not copies — mutate the outer variable later and every closure over it sees the new value.
3. for (var i = 0; i < 3; i++) setTimeout(() => console.log(i)) logs 3, 3, 3. Changing var to let logs 0, 1, 2 because:
- let creates a fresh binding for every iteration, so each callback closes over its own copy of i
- let makes setTimeout run synchronously, so each callback sees the value the loop had at that moment
- let forces the callback to be invoked with the loop variable passed as an implicit argument
- let variables are captured by value while var variables are captured by reference in a closure
Answer: A. The per-iteration binding is specified for let in for statements; the pre-ES2015 fix was an IIFE that took i as a parameter.
4. A counter is built as: const make = () => { let n = 0; return () => ++n; }. Two calls to make() produce counters that:
- share one n, because the arrow function body is created once and reused for both counters
- stay in sync only while both are called from the same event-loop task, then drift apart
- both throw on the second increment, as n is const-like once the outer function has returned
- each hold an independent n, since every call to make creates a new scope and a new binding
Answer: D. One closure per invocation is how factories give each instance private state — the same trick behind the classic module pattern.
5. A function declared inside an if block behaves differently in strict and sloppy mode. What is the difference?
- there is no difference between the two modes, because block-level function declarations were fully standardized in ES2015 with identical semantics
- in strict mode it is a SyntaxError, so the code only parses at all when strict mode is disabled
- in strict mode it is hoisted to the top of the function; in sloppy mode it stays inside the block
- in strict mode the declaration is block-scoped; in sloppy mode Annex B semantics also hoist a var-like binding to the function scope
Answer: D. That web-compatibility carve-out is why the same file can behave differently as a script and as a module, which is always strict.
6. Which of these is a SyntaxError rather than a runtime error?
- reading a let variable before its declaration in the same block
- assigning a new value to a const binding after initialization
- calling a const-declared arrow function before its assignment line
- let x = 1; let x = 2; inside one block
Answer: D. Redeclaring a lexical binding is caught at parse time, so nothing in the file runs. The other three throw ReferenceError or TypeError at runtime.
Drill this domain in practice mode →
Independent community study resource — not affiliated with or endorsed by Oracle, Microsoft or Ecma International. JavaScript is a trademark of Oracle Corporation; TypeScript is a trademark of Microsoft Corporation. All questions and study notes are original, written from MDN, the ECMAScript specification and the TypeScript handbook. Everything runs in your browser; nothing you answer is stored or transmitted.