JavaScript Closures Tutorial - Section 4: Advanced Concepts
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!
The problem occurs because:
var is function-scoped, not block-scopedi variablei is 5i, which is now 5The 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
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'
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
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
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
// ❌ 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);
}
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`);
}
});
// ❌ 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!
// 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)
let or const instead of var in loops// 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);
}
var in loops creates a single shared variablelet creates a new binding per iterationforEach create new scopes automaticallylet/const over varNow 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.