Advanced Closures
A closure is a function bundled together with the variables from the scope where it was created, letting it remember and keep accessing those variables even after that outer function has finished running.
Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.
Part of the free JavaScript course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
Advanced Scope, Closures & Lexical Environments
Master JavaScript's scope system, closures, and lexical environments to become a top 1% developer.
💡 Running Code Locally: While this online editor runs real JavaScript, some advanced examples may have limitations. For the best experience:
JavaScript's scope system is one of the most misunderstood parts of the entire language, especially once you combine closures, async execution, event handlers, and nested functions. Understanding lexical environments is the real key to mastering how JavaScript stores variables, how functions remember old values, how async code captures state, and how real-world bugs like stale data, memory leaks, or broken loops happen.
This lesson will turn you into the top 1% of JS developers by teaching how variables live, die, and evolve inside the engine.
Every time JavaScript runs a block, function, or script, it creates a Lexical Environment . It contains three things:
This is lexical scoping — meaning variables are resolved based on where code is written, not where it's called.
Function Scope
Block Scope
Created by {' '} combined with let or const :
Why does this matter?
Because incorrect scoping is one of the main causes of:
A closure is created whenever a function captures variables from an outer lexical environment even after that environment is gone.
Because the inner function remembers the variable c from the lexical environment of counter() even though counter() already finished executing.
Closures are not "magic" — they are simply preserved lexical environments.
1. Private State (common in frameworks)
2. Debouncing / Throttling (used in UI + API calls)
3. Custom Iterators
If b doesn't exist in second() , JavaScript climbs up.
Because let and const exist in lexical environment but cannot be used before initialization.
Even though global x exists, the block-scoped x "shadows" it, causing TDZ.
Because all async callbacks close over the SAME i .
let creates a NEW lexical environment for each iteration.
Each iteration gets its own lexical environment.
Because the closure captures one shared variable.
React's useState uses closures to maintain values across renders:
Every state value lives inside its own lexical environment.
Closures hold onto variables even when not needed anymore.
big stays in memory because the closure keeps the environment alive.
Understanding lexical environments lets you avoid costly leaks.
Hoisting isn't just about "moving variables to the top." It interacts directly with lexical environments.
Understanding this behaviour makes closure debugging 100× easier.
A closure captures the lexical state when the function is created, not when it is later executed.
Each closure keeps its own snapshot of msg . This is why closures feel like they store memories.
Closures capture variables in lexical scope. this depends on how a function is called.
This one uses this , not closure. Learning the difference is critical for: React classes, Node.js services, Event handlers, Constructor patterns.
A powerful closure concept used in real apps:
This is true private data — not accessible from the outside. Closures enable data encapsulation without classes.
Recursive functions create new execution contexts every call. But shared variables still live in outer lexical environments:
Each recursive call creates a new function context but the closure stores the shared state.
Even after loadUser() finishes, the returned function keeps the lexical environment alive.
Closures keep environment records alive. If a large object is captured inside a closure that lives forever, memory leaks.
Because the closure references the lexical environment, big never gets garbage-collected.
Before ES modules existed, developers used closures to encapsulate logic:
Modules exist because closures made them possible to begin with.
Memoization caches results using closures and can reduce expensive computation costs dramatically.
The closure keeps cache alive and invisible to the outside world.
Debounce:
Throttle:
All of them work because closures store timers & state.
The emitter's internal event registry lives inside a closure.
Each nested function receives its own lexical environment.
Practice quiz
What is a closure?
- A function with no arguments
- A way to close a file handle
- A function bundled with variables from the scope where it was created
- A block that ends with a return
Answer: A function bundled with variables from the scope where it was created. A closure is a function plus the variables from its creation scope, which it keeps accessing even after the outer function finishes.
What does a Lexical Environment's Outer Environment Reference point to?
- The next scope to search when a variable isn't found locally
- The global object only
- The function's return value
- The call stack
Answer: The next scope to search when a variable isn't found locally. The outer environment reference is where JavaScript looks next if a variable isn't found in the current environment record.
What does the counter closure log: const count = counter(); count(); count();
- 0 then 1
- 1 then 1
- undefined then undefined
- 1 then 2
Answer: 1 then 2. The inner function remembers c across calls, so it returns 1 then 2.
With var in a loop, what do three setTimeout callbacks print for i (loop 0..2)?
- 0 1 2
- 3 3 3
- 0 0 0
- 2 2 2
Answer: 3 3 3. All callbacks close over the same shared var i, which is 3 by the time they run.
Why does switching the loop to let fix the bug, printing 0 1 2?
- let creates a new lexical environment per iteration
- let runs faster
- let delays the callbacks
- let copies the value into the timeout
Answer: let creates a new lexical environment per iteration. let creates a fresh binding for each iteration, so each callback closes over its own i.
What is the Temporal Dead Zone (TDZ)?
- A region where var is undefined
- A delay before async code runs
- The time before a let/const variable is initialized, where using it throws
- The garbage collection pause
Answer: The time before a let/const variable is initialized, where using it throws. The TDZ is the span from the start of scope until a let/const is declared, during which accessing it throws a ReferenceError.
Does var have a Temporal Dead Zone?
- Yes, same as let
- No — var is hoisted and initialized to undefined
- Only inside functions
- Only in strict mode
Answer: No — var is hoisted and initialized to undefined. var has no TDZ; it is hoisted and initialized to undefined, so reading it early gives undefined, not an error.
In memoize(fn), where does the cache live?
- On the global object
- In localStorage
- On the fn parameter
- Inside the closure, invisible to the outside
Answer: Inside the closure, invisible to the outside. The cache is kept alive by the returned function's closure and is private to it.
A closure captures variables by...
- value (a copy at creation time)
- reference (it sees later mutations)
- deep clone
- name only
Answer: reference (it sees later mutations). Closures keep references, not copies; setting x = 10 after scheduling a timeout makes it log 10, not 0.
What does multiply(2)(3)(4) return for the curried multiply?
- 9
- 234
- 24
- An error
Answer: 24. Each nested function closes over its argument, so 2 * 3 * 4 is 24.