JavaScript Variables, Scope, and Closure
1. Variable Hoisting and Temporal Dead Zone (TDZ)
| Declaration | Hoisting Behavior | Initialization | TDZ | Access Before Declaration |
|---|---|---|---|---|
| var | Hoisted to function/global scope | Initialized to undefined |
❌ No TDZ | Returns undefined |
| let | Hoisted to block scope | Not initialized | ✓ Has TDZ | ReferenceError |
| const | Hoisted to block scope | Not initialized | ✓ Has TDZ | ReferenceError |
| function (declaration) | Fully hoisted with definition | Fully initialized | ❌ No TDZ | Callable before declaration |
| function (expression) | Variable hoisted only | Depends on var/let/const | Depends on var/let/const | Not callable (undefined or TDZ) |
| class | Hoisted to block scope | Not initialized | ✓ Has TDZ | ReferenceError |
| Concept | Description | Key Points |
|---|---|---|
| Hoisting | JavaScript moves declarations to top of their scope during compilation | Only declarations hoisted, not initializations |
| Temporal Dead Zone (TDZ) | Time between entering scope and variable initialization where access causes error | Exists for let, const, and class |
| TDZ Start | Beginning of enclosing block scope | Not visible in code; conceptual time period |
| TDZ End | Point where variable is declared and initialized | Variable becomes usable after this point |
Example: Hoisting and TDZ behavior
// var hoisting: declaration hoisted, initialized to undefined
console.log(x); // undefined (no error)
var x = 5;
console.log(x); // 5
// let/const TDZ: declaration hoisted but not initialized
// console.log(y); // ReferenceError: Cannot access 'y' before initialization
let y = 10;
console.log(y); // 10
// Function declaration: fully hoisted
greet(); // "Hello" (works before declaration)
function greet() {
console.log("Hello");
}
// Function expression: only variable hoisted
// sayHi(); // TypeError: sayHi is not a function
var sayHi = function() {
console.log("Hi");
};
sayHi(); // "Hi"
// TDZ example with block scope
{
// TDZ starts here for 'temp'
// console.log(temp); // ReferenceError
// const result = doSomething(temp); // ReferenceError
let temp = 5; // TDZ ends here
console.log(temp); // 5 (now accessible)
}
// Class TDZ
// const p = new Person(); // ReferenceError
class Person {
constructor(name) { this.name = name; }
}
const p = new Person("John"); // Works after declaration
Note: TDZ prevents using variables before initialization, catching potential bugs. Always
declare variables at the top of their scope for clarity.
2. Scope Types (Global, Function, Block, Module)
| Scope Type | Created By | Variables | Lifetime | Access |
|---|---|---|---|---|
| Global Scope | Outside all functions/blocks | var, let, const, functions |
Entire program execution | Accessible everywhere |
| Function Scope | Function declarations/expressions | var, parameters, inner functions |
Function execution | Within function and nested functions |
| Block Scope | { } braces, loops, if/switch |
let, const, class |
Block execution | Within block only |
| Module Scope | ES6 modules (.js files with import/export) |
All declarations in module file | Module lifetime | Isolated; explicit exports required |
| Catch Block Scope | catch(error) { } |
Error parameter, let, const |
Catch block execution | Within catch block only |
| Declaration | Global Scope | Function Scope | Block Scope |
|---|---|---|---|
| var | ✓ Function or global | ✓ Function-scoped | ❌ Ignores blocks |
| let | ✓ Global (no window property) | ✓ Function-scoped | ✓ Block-scoped |
| const | ✓ Global (no window property) | ✓ Function-scoped | ✓ Block-scoped |
| function | ✓ Global | ✓ Function-scoped | ❌ Block-scoped in strict mode only |
Example: Different scope types
// Global scope
var globalVar = 'global var';
let globalLet = 'global let';
const globalConst = 'global const';
function demoScopes() {
// Function scope
var functionVar = 'function scoped';
let functionLet = 'function scoped';
console.log(globalVar); // Accessible
if (true) {
// Block scope
var blockVar = 'not block scoped (var)';
let blockLet = 'block scoped';
const blockConst = 'block scoped';
console.log(functionVar); // Accessible from outer function
console.log(blockLet); // Accessible within block
}
console.log(blockVar); // Accessible (var ignores blocks)
// console.log(blockLet); // ReferenceError (block-scoped)
// Loop scope
for (let i = 0; i < 3; i++) {
// 'i' is block-scoped to this loop
setTimeout(() => console.log(i), 100); // Prints 0, 1, 2
}
for (var j = 0; j < 3; j++) {
// 'j' is function-scoped (shared)
setTimeout(() => console.log(j), 100); // Prints 3, 3, 3
}
}
// Module scope (in ES6 modules)
// Variables not automatically global
// export const moduleVar = 'exported';
// const privateVar = 'not exported';
// Catch block scope
try {
throw new Error('test');
} catch (error) {
// 'error' is scoped to catch block
let catchVar = 'catch scoped';
console.log(error.message);
}
// console.log(error); // ReferenceError
// console.log(catchVar); // ReferenceError
Warning: Global variables create properties on the global object (
window in
browsers) with var, but not with let/const. Avoid global variables to
prevent naming conflicts.
3. Lexical Scope and Scope Chain
| Concept | Description | Behavior |
|---|---|---|
| Lexical Scope | Scope determined by code location (where functions are written) | Inner functions access outer function variables |
| Scope Chain | Hierarchy of nested scopes searched for variable resolution | Searches from inner → outer until found or reaches global |
| Static Scoping | Another name for lexical scoping (opposite of dynamic scoping) | Scope determined at write-time, not runtime |
| Outer Environment Reference | Link from execution context to parent scope | Created when function is defined, not when called |
| Variable Lookup Process | Step | Action |
|---|---|---|
| Step 1 | Current Scope | Check if variable exists in current local scope |
| Step 2 | Parent Scope | If not found, move to enclosing (parent) scope |
| Step 3 | Repeat | Continue up the scope chain through ancestors |
| Step 4 | Global Scope | Check global scope as last resort |
| Step 5 | Not Found | ReferenceError if not found anywhere in chain |
Example: Lexical scope and scope chain
// Scope chain demonstration
const globalVar = 'global';
function outer() {
const outerVar = 'outer';
function middle() {
const middleVar = 'middle';
function inner() {
const innerVar = 'inner';
// Scope chain: inner → middle → outer → global
console.log(innerVar); // Found in inner scope
console.log(middleVar); // Found in middle scope (parent)
console.log(outerVar); // Found in outer scope (grandparent)
console.log(globalVar); // Found in global scope
// Variable lookup order:
// 1. Check inner scope
// 2. Check middle scope
// 3. Check outer scope
// 4. Check global scope
// 5. ReferenceError if not found
}
inner();
// console.log(innerVar); // ReferenceError (not in scope chain)
}
middle();
}
outer();
// Lexical scope is determined by code structure
function makeCounter() {
let count = 0; // In outer function scope
return function() {
// Lexically scoped to access 'count' from parent
return ++count;
};
}
const counter1 = makeCounter();
console.log(counter1()); // 1
console.log(counter1()); // 2
const counter2 = makeCounter();
console.log(counter2()); // 1 (separate scope chain)
// Scope determined at definition, not invocation
let x = 'global x';
function showX() {
console.log(x); // Lexically bound to global 'x'
}
function demo() {
let x = 'local x';
showX(); // Prints "global x" (not "local x")
// Scope determined where showX was defined
}
demo();
Note: Lexical scope is determined by where the function is written, not where it's called. This enables closures
and predictable variable access.
4. Variable Shadowing and Name Resolution
| Concept | Description | Behavior | Recommendation |
|---|---|---|---|
| Variable Shadowing | Inner variable hides outer variable with same name | Inner scope variable takes precedence | Avoid shadowing; use different names |
| Name Resolution | Process of finding which variable identifier refers to | Searches scope chain from inner to outer | Use descriptive, unique names |
| Parameter Shadowing | Function parameter shadows outer variable | Parameter accessible, outer variable hidden | Common pattern; usually intentional |
| Block Shadowing | Block-scoped variable shadows outer scope | Only within block; outer restored after | Use for temporary local variables |
| Shadowing Type | Outer | Inner | Allowed? | Behavior |
|---|---|---|---|---|
| var shadows var | var | var | ✓ Yes | Redeclaration in same scope or shadowing in nested |
| let/const shadows var | var | let/const | ✓ Yes | Inner hides outer; outer unchanged |
| var shadows let/const | let/const | var | Depends | Error if same function scope; OK if nested function |
| let/const shadows let/const | let/const | let/const | ✓ In different block | ✗ SyntaxError in same block |
| Parameter shadowing | Any | Parameter | ✓ Yes | Parameter has priority in function |
Example: Variable shadowing scenarios
// Basic shadowing
let x = 'outer';
function demo() {
let x = 'inner'; // Shadows outer 'x'
console.log(x); // "inner"
}
demo();
console.log(x); // "outer" (outer unchanged)
// Parameter shadowing
let value = 100;
function processValue(value) { // Parameter shadows outer 'value'
console.log(value); // Uses parameter, not outer variable
value = 200; // Modifies parameter only
}
processValue(50); // Prints 50
console.log(value); // 100 (outer unchanged)
// Block shadowing
let count = 10;
if (true) {
let count = 20; // Shadows outer 'count' in block
console.log(count); // 20
}
console.log(count); // 10 (outer restored after block)
// Nested function shadowing
function outer() {
let name = 'outer';
function inner() {
let name = 'inner'; // Shadows parent function's 'name'
console.log(name); // "inner"
}
inner();
console.log(name); // "outer"
}
outer();
// Loop variable shadowing
for (let i = 0; i < 3; i++) {
console.log(i); // Loop 'i'
for (let i = 0; i < 2; i++) {
console.log(' ' + i); // Inner loop 'i' shadows outer
}
}
// Problematic shadowing to avoid
function calculateTotal() {
let total = 0;
if (true) {
let total = 100; // Shadowing can be confusing
// Which 'total' is which?
}
return total; // Returns 0, not 100
}
Warning: While shadowing is allowed, excessive use can make code confusing. Use distinct
variable names for clarity unless shadowing is intentional (e.g., function parameters).
5. Closure Patterns and Memory Management
| Concept | Definition | Key Characteristics |
|---|---|---|
| Closure | Function bundled with its lexical environment (surrounding state) | Retains access to outer scope variables even after outer function returns |
| Closure Creation | Created every time a function is created | Inner function "closes over" variables from outer scope |
| Closure Use Cases | Data privacy, factory functions, callbacks, event handlers | Maintains state between function calls |
| Memory Retention | Closed-over variables remain in memory | Can cause memory leaks if not managed properly |
| Closure Pattern | Use Case | Example Structure |
|---|---|---|
| Private Variables | Encapsulation, data hiding | Function returns methods that access private variables |
| Factory Functions | Create objects with private state | Function returns object with methods (closures) |
| Module Pattern | Namespace management, public/private API | IIFE returns object with public methods |
| Callbacks/Event Handlers | Preserve context for async operations | Function captures variables for later execution |
| Currying/Partial Application | Function transformation, configuration | Function returns function with captured arguments |
| Memoization | Performance optimization, caching | Closure maintains cache of computed values |
Example: Common closure patterns
// 1. Private variables pattern
function createCounter() {
let count = 0; // Private variable
return {
increment: () => ++count,
decrement: () => --count,
getCount: () => count
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.getCount()); // 2
// console.log(counter.count); // undefined (private)
// 2. Factory function pattern
function createUser(name) {
let _name = name; // Private
let _sessions = 0;
return {
getName: () => _name,
login: () => ++_sessions,
getSessions: () => _sessions
};
}
const user1 = createUser("Alice");
const user2 = createUser("Bob");
user1.login();
console.log(user1.getSessions()); // 1
console.log(user2.getSessions()); // 0
// 3. Module pattern with IIFE
const calculator = (function() {
let history = []; // Private state
return {
add: (a, b) => {
const result = a + b;
history.push(`${a} + ${b} = ${result}`);
return result;
},
getHistory: () => [...history] // Return copy
};
})();
calculator.add(5, 3);
console.log(calculator.getHistory()); // ["5 + 3 = 8"]
// 4. Callback closure preserving context
function fetchDataWithRetry(url, retries) {
let attempts = 0; // Captured by callback
function attemptFetch() {
fetch(url)
.catch(error => {
if (++attempts < retries) {
console.log(`Retry ${attempts}`);
attemptFetch(); // Closure accesses 'attempts'
}
});
}
attemptFetch();
}
// 5. Currying with closures
function multiply(a) {
return function(b) {
return function(c) {
return a * b * c;
};
};
}
const multiplyBy2 = multiply(2);
const multiplyBy2And3 = multiplyBy2(3);
console.log(multiplyBy2And3(4)); // 24
// 6. Memoization pattern
function memoize(fn) {
const cache = {}; // Closure maintains cache
return function(...args) {
const key = JSON.stringify(args);
if (key in cache) {
return cache[key];
}
const result = fn(...args);
cache[key] = result;
return result;
};
}
const factorial = memoize(n => n <= 1 ? 1 : n * factorial(n - 1));
console.log(factorial(5)); // Computed: 120
console.log(factorial(5)); // Cached: 120
| Memory Consideration | Issue | Solution |
|---|---|---|
| Retained Variables | Closed-over variables can't be garbage collected | Clear references when done (obj = null) |
| Event Listeners | Closure in listener prevents GC of entire scope | Remove listeners: removeEventListener |
| Timers/Intervals | Callback closures keep references alive | Clear timers: clearTimeout/clearInterval |
| Large Data Structures | Closure inadvertently captures large objects | Extract only needed data before creating closure |
Note: Closures are powerful but retain references to their outer scope. Be mindful of memory
usage, especially with long-lived closures or large captured variables.
6. Module Scope and Variable Isolation
| Module Feature | Description | Benefit |
|---|---|---|
| Module Scope | Each ES6 module has its own scope (file-level) | Variables don't pollute global scope by default |
| Variable Isolation | Module variables are private unless explicitly exported | Prevents naming conflicts between modules |
| Strict Mode | Modules automatically run in strict mode | No need for 'use strict'; directive |
| Top-level this | this is undefined at module top level |
Not bound to global object (safer) |
| Static Imports | Import declarations hoisted and executed first | Enables static analysis and tree-shaking |
| Module Pattern | Syntax | Use Case |
|---|---|---|
| Named Export | export const x = 1; |
Export multiple items from module |
| Default Export | export default function() {} |
Main export from module (one per module) |
| Named Import | import { x, y } from './mod'; |
Import specific exports |
| Default Import | import mod from './mod'; |
Import default export |
| Namespace Import | import * as mod from './mod'; |
Import all exports as object |
| Re-export | export { x } from './mod'; |
Export from another module (barrel pattern) |
| Dynamic Import | import('./mod').then(...) |
Load modules conditionally/lazily |
Example: Module scope and isolation
// ===== math.js (Module A) =====
// Private variables (not exported)
const PI = 3.14159;
let calculationCount = 0;
// Private helper function
function log(operation) {
calculationCount++;
console.log(`Operation: ${operation}, Count: ${calculationCount}`);
}
// Named exports (public API)
export function add(a, b) {
log('add');
return a + b;
}
export function multiply(a, b) {
log('multiply');
return a * b;
}
export const TAU = 2 * PI; // Export constant
// Default export
export default function calculate(expr) {
return eval(expr); // Example only
}
// ===== user.js (Module B) =====
// Module variables are isolated
const userCount = 0; // Won't conflict with other modules
class User {
constructor(name) {
this.name = name;
}
}
export { User, userCount };
// ===== app.js (Main module) =====
// Import from math.js
import calculate, { add, multiply, TAU } from './math.js';
// Import from user.js
import { User, userCount } from './user.js';
// Module-scoped variables (private to this module)
const appName = 'MyApp';
let config = { debug: true };
console.log(add(5, 3)); // 8
console.log(multiply(2, 4)); // 8
console.log(TAU); // 6.28318
// Cannot access private variables from math.js
// console.log(PI); // ReferenceError
// console.log(calculationCount); // ReferenceError
const user = new User('Alice');
// Top-level 'this' is undefined in modules
console.log(this); // undefined (not window)
// ===== Dynamic imports =====
async function loadModule() {
if (config.debug) {
const debugModule = await import('./debug.js');
debugModule.log('Debug mode enabled');
}
}
// ===== Barrel export pattern (index.js) =====
// Re-export from multiple modules
export { add, multiply } from './math.js';
export { User } from './user.js';
export { default as calculate } from './math.js';
Note: ES6 modules provide true encapsulation with file-level scope. Variables are private by
default, eliminating need for IIFE patterns and reducing global scope pollution.
Section 3 Summary
- Hoisting moves declarations to top of scope;
let/const/classhave TDZ (ReferenceError before initialization) - 4 scope types: Global (everywhere), Function (var), Block (let/const), Module (file-level isolation)
- Lexical scope determined by code structure; scope chain searches inner → outer → global for variables
- Variable shadowing occurs when inner scope variable hides outer one; use distinct names for clarity
- Closures retain access to outer scope variables; enable private state but require memory management
- ES6 modules provide true isolation with file-level scope; variables private unless exported