Learning Objectives

  • Understand the classic closure-in-loop problem
  • Learn multiple solutions to fix the issue
  • Understand var vs let in loops
  • Apply best practices for loops with closures

The Classic Problem

One of the most common closure pitfalls occurs when creating closures inside loops. Here's the problem:

// ❌ Problem: All buttons log 5
for (var i = 0; i < 5; i++) {
    const button = document.createElement('button');
    button.textContent = `Button ${i}`;
    
    button.addEventListener('click', function() {
        console.log(`Button ${i} clicked`);
    });
    
    document.body.appendChild(button);
}

// When you click any button, it logs "Button 5 clicked"
// Why? Because all closures share the same 'i' variable!

Why Does This Happen?

The problem occurs because:

  1. var is function-scoped, not block-scoped
  2. All event handlers share the same i variable
  3. By the time any button is clicked, the loop has finished and i is 5
  4. All closures reference the same i, which is now 5

Solution 1: Use let (ES6+)

The simplest and most modern solution:

// ✅ Solution: Use let instead of var
for (let i = 0; i < 5; i++) {
    const button = document.createElement('button');
    button.textContent = `Button ${i}`;
    
    button.addEventListener('click', function() {
        console.log(`Button ${i} clicked`);
    });
    
    document.body.appendChild(button);
}

// Each iteration creates a new block scope with its own 'i'
// Each closure captures its own 'i' value

Solution 2: IIFE (Immediately Invoked Function Expression)

Create a new scope for each iteration:

// ✅ Solution: Use IIFE to create new scope
for (var i = 0; i < 5; i++) {
    (function(index) {
        const button = document.createElement('button');
        button.textContent = `Button ${index}`;
        
        button.addEventListener('click', function() {
            console.log(`Button ${index} clicked`);
        });
        
        document.body.appendChild(button);
    })(i);
}

// The IIFE creates a new scope with its own 'index' parameter
// Each closure captures its own 'index'

Solution 3: Factory Function

Extract the closure creation into a function:

// ✅ Solution: Use factory function
function createClickHandler(index) {
    return function() {
        console.log(`Button ${index} clicked`);
    };
}

for (var i = 0; i < 5; i++) {
    const button = document.createElement('button');
    button.textContent = `Button ${i}`;
    button.addEventListener('click', createClickHandler(i));
    document.body.appendChild(button);
}

// Each call to createClickHandler creates a new closure
// with its own 'index' parameter

Solution 4: Array.forEach

Use array methods that create new scopes:

// ✅ Solution: Use forEach
[0, 1, 2, 3, 4].forEach(function(i) {
    const button = document.createElement('button');
    button.textContent = `Button ${i}`;
    
    button.addEventListener('click', function() {
        console.log(`Button ${i} clicked`);
    });
    
    document.body.appendChild(button);
});

// forEach callback creates a new scope for each iteration
// Each closure has its own 'i' parameter

Solution 5: bind()

Use bind to create a new function with fixed parameters:

// ✅ Solution: Use bind
function handleClick(index) {
    console.log(`Button ${index} clicked`);
}

for (var i = 0; i < 5; i++) {
    const button = document.createElement('button');
    button.textContent = `Button ${i}`;
    button.addEventListener('click', handleClick.bind(null, i));
    document.body.appendChild(button);
}

// bind creates a new function with 'i' bound as first argument

Practical Example: setTimeout in Loop

// ❌ Problem: All log 5
for (var i = 0; i < 5; i++) {
    setTimeout(function() {
        console.log(i);
    }, i * 1000);
}

// ✅ Solution 1: Use let
for (let i = 0; i < 5; i++) {
    setTimeout(function() {
        console.log(i);
    }, i * 1000);
}

// ✅ Solution 2: IIFE
for (var i = 0; i < 5; i++) {
    (function(index) {
        setTimeout(function() {
            console.log(index);
        }, index * 1000);
    })(i);
}

// ✅ Solution 3: Pass parameter to setTimeout
for (var i = 0; i < 5; i++) {
    setTimeout(function(index) {
        console.log(index);
    }, i * 1000, i);
}

Practical Example: Event Delegation (Alternative Approach)

Sometimes you can avoid the problem entirely with event delegation:

// Create buttons without individual handlers
for (let i = 0; i < 5; i++) {
    const button = document.createElement('button');
    button.textContent = `Button ${i}`;
    button.dataset.index = i; // Store index in data attribute
    document.body.appendChild(button);
}

// Single delegated handler
document.body.addEventListener('click', function(e) {
    if (e.target.tagName === 'BUTTON') {
        const index = e.target.dataset.index;
        console.log(`Button ${index} clicked`);
    }
});

Practical Example: Creating Multiple Counters

// ❌ Problem: All counters share the same count
const counters = [];
for (var i = 0; i < 3; i++) {
    counters.push({
        increment: function() {
            i++;
            return i;
        },
        get: function() {
            return i;
        }
    });
}

console.log(counters[0].increment()); // 4
console.log(counters[1].increment()); // 5
console.log(counters[2].increment()); // 6
// All share the same 'i'!

// ✅ Solution: Use let or factory function
function createCounter(initial) {
    let count = initial;
    return {
        increment: function() {
            count++;
            return count;
        },
        get: function() {
            return count;
        }
    };
}

const counters2 = [];
for (let i = 0; i < 3; i++) {
    counters2.push(createCounter(i));
}

console.log(counters2[0].increment()); // 1
console.log(counters2[1].increment()); // 2
console.log(counters2[2].increment()); // 3
// Each has its own count!

Understanding var vs let in Loops

// var: Single binding for entire loop
for (var i = 0; i < 3; i++) {
    // All iterations share the same 'i'
}
console.log(i); // 3 (accessible outside loop)

// let: New binding for each iteration
for (let j = 0; j < 3; j++) {
    // Each iteration has its own 'j'
}
console.log(j); // ReferenceError (not accessible outside loop)

Best Practices

  • Use let or const instead of var in loops
  • Extract closure creation into factory functions
  • Use array methods like forEach, map, filter when possible
  • Consider event delegation to avoid creating many handlers
  • Test your closures to ensure they capture the right values

Debugging Closure Issues

// Add logging to see what's captured
for (let i = 0; i < 3; i++) {
    const handler = function() {
        console.log('Captured i:', i);
    };
    
    // Log immediately to verify
    console.log('Creating handler for i:', i);
    
    setTimeout(handler, 1000);
}

Key Takeaways

  • var in loops creates a single shared variable
  • let creates a new binding per iteration
  • ✅ Use IIFE or factory functions to create new scopes
  • ✅ Array methods like forEach create new scopes automatically
  • Event delegation can avoid the problem entirely
  • ✅ Always prefer let/const over var

Next Steps

Now that you understand the closure-in-loop pitfall, in the next lesson we'll explore memory management and how to avoid memory leaks with closures.