JavaScript: Scope and Closures
Understand where variables live, how JavaScript looks them up, and how closures let functions remember their environment.
Global Scope
A variable declared outside of any function or block lives in the global scope and is accessible from anywhere in the script.
Global Scope
Global variables are convenient but risky: any code can read or overwrite them.
- Variables declared with
varat the top level become properties ofwindowin browsers - Top-level
letandconstare global in reach but not attached towindow - Avoid polluting global scope: name collisions across scripts cause hard-to-trace bugs
Global Scope Example
A top-level variable is readable inside any function.
Press Run to execute the code and see output here.
Function Scope
Variables declared with var (or let / const) inside a function are local to that function and cannot be accessed outside it.
Function Scope
Each function creates its own private scope, protecting its variables from the outside world.
- A variable declared inside a function disappears when the function returns
varis scoped to the nearest enclosing function, not to blocks- Function scope is the reason helper variables do not leak into global scope
Function Scope Example
rate is private to calculateTax and invisible outside it.
Press Run to execute the code and see output here.
Block Scope (let and const)
let and const are scoped to the nearest pair of curly braces {}, including if blocks, loops, and standalone blocks.
Block Scope
Block scope prevents loop and if-statement variables from leaking into the surrounding function.
varignores block boundaries, it leaks out of if/for blocks into the functionletandconstare confined to the block{ }they are declared in- Prefer
let/constovervarto avoid scope surprises
Block Scope vs var
let stays inside the block; var leaks into the enclosing function.
Press Run to execute the code and see output here.
Lexical Scope
JavaScript uses lexical (static) scope: a function's scope is determined by where it is written in the source code, not where or how it is called.
Lexical Scope
The scope of a function is fixed at write time, not at call time.
- An inner function can always read variables from its outer function
- The lookup happens at parse time based on the code structure
- This is the foundation that makes closures possible
Lexical Scope Example
inner() sees language and level because of where it is written, not where it is called.
Press Run to execute the code and see output here.
Scope Chain
When JavaScript looks up a variable, it starts in the current scope and works outward through each enclosing scope until it finds the variable or reaches global scope.
Scope Chain
The scope chain is the ordered list of scopes JavaScript searches when resolving a variable name.
- Search order: current scope → outer function scope → ... → global scope
- If the variable is not found anywhere, a
ReferenceErroris thrown - The chain only goes outward: inner scopes are not visible to outer ones
Scope Chain Lookup
inner() walks the chain outward to find a and b.
Press Run to execute the code and see output here.
What is a Closure?
A closure is a function that retains access to its outer scope variables even after the outer function has finished executing. The function "closes over" its surrounding environment.
Closure
A closure bundles a function together with the variables from the scope where it was defined.
- The outer function runs and returns, but its variables are not garbage-collected if an inner function still references them
- Every function in JavaScript is technically a closure
- Closures are what allow callbacks and event handlers to remember data from their creation context
Basic Closure
Each returned function remembers its own name from when makeGreeter was called.
Press Run to execute the code and see output here.
How Closures Work
Each call to the outer function creates a new scope with its own copy of the variables. The returned inner function holds a reference to that specific scope.
Closure Mechanics
The inner function holds a live reference to the outer scope, not a snapshot copy.
- Each outer function call produces an independent closure environment
- The inner function reads the current value of the outer variable, not the value at creation time
- This is why closures over loop variables with
varbehave unexpectedly, all share one variable
Independent Closure Environments
c1 and c2 each close over their own count variable.
Press Run to execute the code and see output here.
Practical Uses of Closures
Closures appear constantly in JavaScript: callbacks, event handlers, timers, and factory functions all rely on them.
Common Closure Use Cases
Closures let you attach persistent state to a function without a class.
- Event handlers that need access to variables from the setup code
- setTimeout / setInterval callbacks that reference outer data
- Factory functions that produce pre-configured functions
- Memoization: caching function results in a closed-over object
Closures in Practice
A multiplier factory and a delayed logger both use closures to capture their context.
Press Run to execute the code and see output here.
Private Variables with Closures
Because inner functions can read outer variables but outside code cannot, closures are the classic JavaScript way to create truly private state.
Private State via Closures
Variables in the outer function scope are inaccessible to any code outside the returned object.
- Only the functions returned by the outer function can read or mutate the private variable
- Outside code has no way to reach the variable directly
- This pattern predates the
#privateFieldclass syntax and still works everywhere
Private Balance
balance is only readable through the returned methods, not directly.
Press Run to execute the code and see output here.
Module Pattern
The module pattern wraps related code in an immediately-invoked function expression (IIFE) and returns only the public API, keeping internals private.
Module Pattern
The module pattern uses a closure to simulate a module with public and private members.
- An IIFE runs immediately and its scope is the private closure environment
- Only properties on the returned object are public
- Widely used before ES modules (
import/export) were available - Still useful for quick encapsulation in scripts that do not use a bundler
Module Pattern Example
items and total() are private; only add, getTotal, and itemCount are exposed.
Press Run to execute the code and see output here.
Variable Shadowing
When an inner scope declares a variable with the same name as one in an outer scope, the inner declaration shadows the outer one within that scope.
Variable Shadowing
Shadowing is not an error, but accidental shadowing causes subtle bugs where you expect to see the outer value.
- The outer variable is not changed, it is simply hidden inside the inner scope
- Once you leave the inner scope, the outer variable is visible again at its original value
- Intentional shadowing is fine; accidental shadowing (parameter names matching globals) is a common mistake
Variable Shadowing
The inner color hides the outer one, but only within paintWall's scope.
Press Run to execute the code and see output here.
Knowledge Check
1. Where is a variable declared with var at the top level of a script accessible?
2. Which keywords create block scope?
3. What is lexical scope?
4. What is a closure?
5. What does the scope chain do when a variable is not found in the current scope?
6. Which pattern uses closures to hide internal variables from outside code?
7. What is variable shadowing?