Variables, Scope, and Closure

1. Variable Hoisting and Temporal Dead Zone

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';

Module Benefits:

  • Encapsulation: private implementation details
  • Explicit dependencies: clear import statements
  • No global pollution: isolated scope
  • Reusability: modular, composable code
  • Static analysis: enables tree-shaking

Before Modules (Legacy):

// IIFE pattern for isolation
(function() {
    var private = 'hidden';
    
    window.MyModule = {
        public: function() {
            return private;
        }
    };
})();
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/class have 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