Scope and closures are fundamental concepts that determine where variables can be accessed and how functions remember their environment. Understanding scope helps you avoid bugs and write cleaner code, while closures unlock powerful patterns like data privacy, factory functions, and functional programming. Let's master these essential JavaScript concepts!
What Is Scope?
Scope determines where variables are accessible in your code. It's the context in which variables exist and can be referenced.
Think of scope like a building:
- Global scope: The lobby - accessible to everyone
- Function scope: Private offices - only accessible inside
- Block scope: Meeting rooms - only accessible while inside
Three Types of Scope
// Global scope - accessible everywhere
let globalVar = "I'm global";
function myFunction() {
// Function scope - only accessible inside this function
let functionVar = "I'm in the function";
if (true) {
// Block scope - only accessible inside this block
let blockVar = "I'm in the block";
console.log(globalVar); // ✓ Can access
console.log(functionVar); // ✓ Can access
console.log(blockVar); // ✓ Can access
}
console.log(globalVar); // ✓ Can access
console.log(functionVar); // ✓ Can access
// console.log(blockVar); // ✗ Error! Not accessible here
}
console.log(globalVar); // ✓ Can access
// console.log(functionVar); // ✗ Error! Not accessible here
// console.log(blockVar); // ✗ Error! Not accessible hereGlobal Scope
Variables declared outside any function or block are in the global scope and accessible everywhere in your code.
// Global variables
let appName = "MyApp";
let version = "1.0";
const MAX_USERS = 100;
function displayInfo() {
console.log(`${appName} v${version}`);
console.log(`Max users: ${MAX_USERS}`);
}
function updateVersion(newVersion) {
version = newVersion; // Can modify global variable
}
displayInfo(); // "MyApp v1.0"
updateVersion("2.0");
displayInfo(); // "MyApp v2.0"Global Scope Problems
// Problem 1: Name collisions
let name = "Global Name";
function greet() {
let name = "Function Name";
console.log(name); // Which name?
}
greet(); // "Function Name" (local shadows global)
// Problem 2: Unintentional modifications
let counter = 0;
function increment() {
counter++; // Modifies global
}
function reset() {
counter = 0; // Also modifies global
}
increment();
console.log(counter); // 1
reset();
console.log(counter); // 0
// Problem 3: Pollution
// Multiple scripts can accidentally overwrite globals
let data = "Important data";
// Later in another script...
// let data = "Different data"; // Overwrites!Best Practice: Minimize global variables to avoid naming conflicts and unintended modifications. Use functions and modules to create private scopes.
Function Scope
Variables declared inside a function are only accessible within that function.
function calculateTotal() {
// These variables only exist inside this function
let price = 100;
let tax = 8;
let total = price + tax;
return total;
}
console.log(calculateTotal()); // 108
// console.log(price); // Error! price not accessible
// console.log(tax); // Error! tax not accessible
// console.log(total); // Error! total not accessible
// Each function has its own scope
function function1() {
let message = "Hello from function1";
console.log(message); // Works
}
function function2() {
let message = "Hello from function2";
console.log(message); // Works (different scope)
}
function1(); // "Hello from function1"
function2(); // "Hello from function2"
// Each function's 'message' is separateNested Function Scopes
function outer() {
let outerVar = "I'm outer";
function inner() {
let innerVar = "I'm inner";
// Inner can access outer
console.log(outerVar); // "I'm outer"
console.log(innerVar); // "I'm inner"
}
inner();
// Outer cannot access inner
console.log(outerVar); // "I'm outer"
// console.log(innerVar); // Error! Not accessible
}
outer();
// Scope chain: inner → outer → global
function level1() {
let l1 = "Level 1";
function level2() {
let l2 = "Level 2";
function level3() {
let l3 = "Level 3";
// Can access all outer scopes
console.log(l1); // "Level 1"
console.log(l2); // "Level 2"
console.log(l3); // "Level 3"
}
level3();
}
level2();
}
level1();Block Scope
Variables declared with let and const inside curly braces {} are block-scoped.
// Block scope with let and const
{
let blockVar = "I'm in a block";
const BLOCK_CONST = "Also in block";
console.log(blockVar); // Works
console.log(BLOCK_CONST); // Works
}
// console.log(blockVar); // Error! Outside block
// console.log(BLOCK_CONST); // Error! Outside block
// Block scope in if statements
if (true) {
let insideIf = "Only in if";
console.log(insideIf); // Works
}
// console.log(insideIf); // Error!
// Block scope in loops
for (let i = 0; i < 3; i++) {
let loopVar = "In loop";
console.log(i, loopVar); // Works
}
// console.log(i); // Error!
// console.log(loopVar); // Error!var vs let/const
let and const provide better scoping
var (Function Scope)
// var has function scope (old way)
function testVar() {
if (true) {
var x = 10;
}
console.log(x); // 10 - accessible!
}
testVar();
// var in loops
for (var i = 0; i < 3; i++) {
// var is function-scoped
}
console.log(i); // 3 - still accessible!
// Problem: var leaks out of blocks
if (true) {
var leaked = "I leaked out";
}
console.log(leaked); // "I leaked out"let/const (Block Scope)
// let has block scope (modern)
function testLet() {
if (true) {
let x = 10;
}
// console.log(x); // Error! Not accessible
}
testLet();
// let in loops
for (let i = 0; i < 3; i++) {
// let is block-scoped
}
// console.log(i); // Error! Not accessible
// let stays in block
if (true) {
let contained = "I'm contained";
}
// console.log(contained); // Error!Lexical Scope
Lexical scope means that scope is determined by where variables are written in the code, not where they're called.
let globalVar = "global";
function outer() {
let outerVar = "outer";
function inner() {
let innerVar = "inner";
// Can access variables from where function is DEFINED
console.log(innerVar); // "inner" (own scope)
console.log(outerVar); // "outer" (parent scope)
console.log(globalVar); // "global" (global scope)
}
return inner;
}
let myFunction = outer();
myFunction(); // Has access to outer's variables!
// Another example
function makeCounter() {
let count = 0; // In makeCounter's scope
return function() {
count++; // Can access parent's count
return count;
};
}
let counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
// The inner function remembers 'count' from where it was definedScope Chain
let level0 = "Global";
function level1Func() {
let level1 = "Level 1";
function level2Func() {
let level2 = "Level 2";
function level3Func() {
let level3 = "Level 3";
// JavaScript looks up the scope chain
console.log(level3); // Found in current scope
console.log(level2); // Look up one level
console.log(level1); // Look up two levels
console.log(level0); // Look up to global
}
level3Func();
}
level2Func();
}
level1Func();
// Scope chain: level3 → level2 → level1 → global
// JavaScript searches from innermost to outermostClosures
A closure is a function that has access to variables from its outer scope, even after the outer function has finished executing.
Closure = Function + Lexical Environment
When a function is created, it "remembers" the variables from the scope where it was created. This combination of the function and its remembered environment is a closure.
Basic Closure Example
function createGreeter(greeting) {
// greeting is in outer scope
return function(name) {
// Inner function "closes over" greeting
return `${greeting}, ${name}!`;
};
}
let sayHello = createGreeter("Hello");
let sayHi = createGreeter("Hi");
console.log(sayHello("Alice")); // "Hello, Alice!"
console.log(sayHi("Bob")); // "Hi, Bob!"
// Even though createGreeter finished executing,
// the returned functions still have access to their 'greeting'How Closures Work
function outer() {
let count = 0; // Variable in outer scope
function inner() {
count++; // Inner function can access count
console.log(count);
}
return inner;
}
let increment = outer();
// outer() has finished, but...
increment(); // 1 - still has access to count!
increment(); // 2
increment(); // 3
// The inner function maintains a reference to count
// This is a closure!Multiple Closures
function makeCounter() {
let count = 0;
return {
increment: function() {
count++;
return count;
},
decrement: function() {
count--;
return count;
},
getCount: function() {
return count;
}
};
}
let counter1 = makeCounter();
let counter2 = makeCounter();
console.log(counter1.increment()); // 1
console.log(counter1.increment()); // 2
console.log(counter1.getCount()); // 2
console.log(counter2.increment()); // 1
console.log(counter2.getCount()); // 1
// Each counter has its own separate 'count'Practical Closure Patterns
Pattern 1: Data Privacy
function createBankAccount(initialBalance) {
// Private variable - not accessible from outside
let balance = initialBalance;
// Public methods - can access private variable
return {
deposit: function(amount) {
if (amount > 0) {
balance += amount;
return `Deposited $${amount}. New balance: $${balance}`;
}
return "Invalid amount";
},
withdraw: function(amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
return `Withdrew $${amount}. New balance: $${balance}`;
}
return "Invalid amount or insufficient funds";
},
getBalance: function() {
return `Current balance: $${balance}`;
}
};
}
let account = createBankAccount(1000);
console.log(account.deposit(500)); // "Deposited $500..."
console.log(account.withdraw(200)); // "Withdrew $200..."
console.log(account.getBalance()); // "Current balance: $1300"
// Can't access balance directly
// console.log(account.balance); // undefined (private!)Pattern 2: Function Factory
function createMultiplier(multiplier) {
return function(number) {
return number * multiplier;
};
}
let double = createMultiplier(2);
let triple = createMultiplier(3);
let quadruple = createMultiplier(4);
console.log(double(5)); // 10
console.log(triple(5)); // 15
console.log(quadruple(5)); // 20
// Each function remembers its own multiplier
function createFormatter(prefix, suffix) {
return function(text) {
return `${prefix}${text}${suffix}`;
};
}
let addBrackets = createFormatter("[", "]");
let addParens = createFormatter("(", ")");
let addQuotes = createFormatter('"', '"');
console.log(addBrackets("Hello")); // "[Hello]"
console.log(addParens("Hello")); // "(Hello)"
console.log(addQuotes("Hello")); // '"Hello"'Pattern 3: Maintaining State
function createIdGenerator(prefix) {
let nextId = 1;
return function() {
return `${prefix}-${nextId++}`;
};
}
let generateUserId = createIdGenerator("USER");
let generateOrderId = createIdGenerator("ORDER");
console.log(generateUserId()); // "USER-1"
console.log(generateUserId()); // "USER-2"
console.log(generateUserId()); // "USER-3"
console.log(generateOrderId()); // "ORDER-1"
console.log(generateOrderId()); // "ORDER-2"
// Each generator maintains its own counter
function createToggle() {
let isOn = false;
return function() {
isOn = !isOn;
return isOn ? "ON" : "OFF";
};
}
let lightSwitch = createToggle();
console.log(lightSwitch()); // "ON"
console.log(lightSwitch()); // "OFF"
console.log(lightSwitch()); // "ON"Pattern 4: Partial Application
function makeAdder(x) {
return function(y) {
return x + y;
};
}
let add5 = makeAdder(5);
let add10 = makeAdder(10);
console.log(add5(3)); // 8
console.log(add5(7)); // 12
console.log(add10(3)); // 13
console.log(add10(7)); // 17
// More complex example
function createPriceCalculator(taxRate, discount) {
return function(price) {
let afterDiscount = price * (1 - discount);
let withTax = afterDiscount * (1 + taxRate);
return withTax.toFixed(2);
};
}
let calculateUSPrice = createPriceCalculator(0.08, 0.1);
let calculateEUPrice = createPriceCalculator(0.20, 0.05);
console.log(calculateUSPrice(100)); // $97.20
console.log(calculateEUPrice(100)); // $114.00Common Closure Mistakes
Mistake 1: Loop with var
// Problem: Using var in loops
function createFunctions() {
let functions = [];
for (var i = 0; i < 3; i++) {
functions.push(function() {
console.log(i); // All share same 'i'
});
}
return functions;
}
let funcs = createFunctions();
funcs[0](); // 3 (not 0!)
funcs[1](); // 3 (not 1!)
funcs[2](); // 3 (not 2!)
// Solution 1: Use let (block-scoped)
function createFunctionsBetter() {
let functions = [];
for (let i = 0; i < 3; i++) { // let instead of var
functions.push(function() {
console.log(i); // Each has its own 'i'
});
}
return functions;
}
let betterFuncs = createFunctionsBetter();
betterFuncs[0](); // 0
betterFuncs[1](); // 1
betterFuncs[2](); // 2
// Solution 2: IIFE (old way)
function createFunctionsIIFE() {
let functions = [];
for (var i = 0; i < 3; i++) {
(function(index) {
functions.push(function() {
console.log(index);
});
})(i);
}
return functions;
}Mistake 2: Memory Leaks
// Closures can prevent garbage collection
function createHeavyFunction() {
let largeData = new Array(1000000).fill("data");
return function() {
// This keeps largeData in memory
console.log(largeData.length);
};
}
// Solution: Only close over what you need
function createLightFunction() {
let largeData = new Array(1000000).fill("data");
let length = largeData.length; // Extract only what's needed
return function() {
// Only closes over length, not largeData
console.log(length);
};
}Practical Examples
Example 1: Event Counter
function createEventTracker() {
let events = {};
return {
track: function(eventName) {
events[eventName] = (events[eventName] || 0) + 1;
return `Tracked: ${eventName}`;
},
getCount: function(eventName) {
return events[eventName] || 0;
},
getAll: function() {
return { ...events };
},
reset: function() {
events = {};
return "All events cleared";
}
};
}
let tracker = createEventTracker();
tracker.track("click");
tracker.track("click");
tracker.track("hover");
tracker.track("click");
console.log(tracker.getCount("click")); // 3
console.log(tracker.getCount("hover")); // 1
console.log(tracker.getAll());
// { click: 3, hover: 1 }Example 2: Rate Limiter
function createRateLimiter(maxCalls, timeWindow) {
let calls = [];
return function(fn) {
let now = Date.now();
// Remove old calls outside time window
calls = calls.filter(time => now - time < timeWindow);
if (calls.length < maxCalls) {
calls.push(now);
return fn();
} else {
return "Rate limit exceeded. Try again later.";
}
};
}
// Allow 3 calls per 5 seconds
let limitedFunction = createRateLimiter(3, 5000);
function apiCall() {
return "API call successful";
}
console.log(limitedFunction(apiCall)); // "API call successful"
console.log(limitedFunction(apiCall)); // "API call successful"
console.log(limitedFunction(apiCall)); // "API call successful"
console.log(limitedFunction(apiCall)); // "Rate limit exceeded..."Scope and Closures Practice
Experiment with scope and closure patterns
console.log() to see your output in the console above.Key Takeaways
- Scope determines where variables are accessible in code
- Three types: global scope, function scope, block scope
letandconsthave block scope;varhas function scope- Lexical scope: determined by where code is written, not where it's called
- Closures: functions that remember variables from outer scope
- Inner functions can access outer function variables
- Closures enable data privacy and encapsulation
- Common patterns: private variables, function factories, state management
- Use
letin loops to avoid closure bugs - Closures are powerful but can affect memory if not careful
What's Next?
You now understand scope and closures—two of the most important concepts in JavaScript! These concepts enable powerful patterns and are essential for writing advanced JavaScript code.
In the final lesson of the Functions section, we'll explore Higher-Order Functions and Callbacks—functions that accept or return other functions. This enables functional programming patterns and is crucial for modern JavaScript development!
💪 Practice Challenge:
Before moving on, try creating:
- A counter with increment, decrement, and reset using closures
- A function factory that creates customized greeting functions
- A private variable system for storing user settings
- An ID generator that creates unique IDs with a prefix
- A simple cache system using closures to store and retrieve values