JavaScript Closures Tutorial - Section 3: Common Use Cases
setTimeout callbacks create closures that remember their surrounding context:
function delayedGreeting(name, delay) {
console.log(`Setting up greeting for ${name}`);
setTimeout(function() {
// Closure: has access to name and delay
console.log(`Hello, ${name}! (after ${delay}ms)`);
}, delay);
}
delayedGreeting('Alice', 1000);
delayedGreeting('Bob', 2000);
A classic closure problem and its solutions:
// ❌ Problem: All timeouts log 5
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i); // Logs 5, five times
}, i * 1000);
}
// ✅ Solution 1: Use let (block scope)
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i); // Logs 0, 1, 2, 3, 4
}, i * 1000);
}
// ✅ Solution 2: Create closure with IIFE
for (var i = 0; i < 5; i++) {
(function(index) {
setTimeout(function() {
console.log(index); // Logs 0, 1, 2, 3, 4
}, index * 1000);
})(i);
}
// ✅ Solution 3: Pass parameter to setTimeout
for (var i = 0; i < 5; i++) {
setTimeout(function(index) {
console.log(index); // Logs 0, 1, 2, 3, 4
}, i * 1000, i);
}
function createCountdown(seconds) {
let remaining = seconds;
let intervalId = null;
let isPaused = false;
return {
start: function(callback) {
if (intervalId) return; // Already running
intervalId = setInterval(() => {
if (!isPaused) {
remaining--;
callback(remaining);
if (remaining <= 0) {
this.stop();
}
}
}, 1000);
},
pause: function() {
isPaused = true;
},
resume: function() {
isPaused = false;
},
stop: function() {
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
}
},
reset: function() {
this.stop();
remaining = seconds;
},
getRemaining: function() {
return remaining;
}
};
}
const timer = createCountdown(10);
timer.start((remaining) => {
console.log(`${remaining} seconds left`);
if (remaining === 0) {
console.log('Time\'s up!');
}
});
function createStopwatch() {
let startTime = null;
let elapsedTime = 0;
let intervalId = null;
let isRunning = false;
return {
start: function() {
if (isRunning) return;
isRunning = true;
startTime = Date.now() - elapsedTime;
intervalId = setInterval(() => {
elapsedTime = Date.now() - startTime;
}, 10);
},
stop: function() {
if (!isRunning) return;
isRunning = false;
clearInterval(intervalId);
},
reset: function() {
this.stop();
elapsedTime = 0;
},
getTime: function() {
const total = Math.floor(elapsedTime / 1000);
const hours = Math.floor(total / 3600);
const minutes = Math.floor((total % 3600) / 60);
const seconds = total % 60;
const ms = Math.floor((elapsedTime % 1000) / 10);
return {
hours,
minutes,
seconds,
milliseconds: ms,
formatted: `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}.${String(ms).padStart(2, '0')}`
};
},
isRunning: function() {
return isRunning;
}
};
}
const stopwatch = createStopwatch();
stopwatch.start();
setTimeout(() => {
console.log(stopwatch.getTime().formatted);
stopwatch.stop();
}, 5000);
function retryWithBackoff(fn, maxRetries = 3, baseDelay = 1000) {
let attempt = 0;
function tryExecute() {
return fn().catch(error => {
attempt++;
if (attempt >= maxRetries) {
throw new Error(`Failed after ${maxRetries} attempts: ${error.message}`);
}
const delay = baseDelay * Math.pow(2, attempt - 1);
console.log(`Retry ${attempt} after ${delay}ms`);
return new Promise(resolve => {
setTimeout(() => {
resolve(tryExecute());
}, delay);
});
});
}
return tryExecute();
}
// Usage
async function unreliableAPI() {
if (Math.random() < 0.7) {
throw new Error('API failed');
}
return 'Success!';
}
retryWithBackoff(unreliableAPI, 5, 1000)
.then(result => console.log(result))
.catch(error => console.error(error.message));
function createPoller(fn, interval) {
let intervalId = null;
let isPolling = false;
let lastResult = null;
return {
start: function() {
if (isPolling) return;
isPolling = true;
// Execute immediately
fn().then(result => {
lastResult = result;
});
// Then poll at interval
intervalId = setInterval(() => {
fn().then(result => {
lastResult = result;
});
}, interval);
},
stop: function() {
if (!isPolling) return;
isPolling = false;
clearInterval(intervalId);
},
getLastResult: function() {
return lastResult;
},
isPolling: function() {
return isPolling;
}
};
}
// Poll API every 5 seconds
const poller = createPoller(
() => fetch('/api/status').then(r => r.json()),
5000
);
poller.start();
// Stop after 30 seconds
setTimeout(() => {
poller.stop();
console.log('Final result:', poller.getLastResult());
}, 30000);
function debounce(fn, delay, immediate = false) {
let timeoutId;
return function(...args) {
const callNow = immediate && !timeoutId;
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
timeoutId = null;
if (!immediate) {
fn.apply(this, args);
}
}, delay);
if (callNow) {
fn.apply(this, args);
}
};
}
// Execute immediately on first call, then debounce
const search = debounce((query) => {
console.log('Searching:', query);
}, 300, true);
function createAnimationTimer(duration, onUpdate, onComplete) {
let startTime = null;
let rafId = null;
let isPaused = false;
let pausedTime = 0;
function animate(currentTime) {
if (!startTime) startTime = currentTime;
if (isPaused) {
rafId = requestAnimationFrame(animate);
return;
}
const elapsed = currentTime - startTime - pausedTime;
const progress = Math.min(elapsed / duration, 1);
onUpdate(progress);
if (progress < 1) {
rafId = requestAnimationFrame(animate);
} else {
if (onComplete) onComplete();
}
}
return {
start: function() {
rafId = requestAnimationFrame(animate);
},
pause: function() {
isPaused = true;
pausedTime = Date.now() - startTime;
},
resume: function() {
isPaused = false;
startTime = Date.now() - pausedTime;
},
stop: function() {
if (rafId) {
cancelAnimationFrame(rafId);
}
}
};
}
// Animate element over 2 seconds
const element = document.getElementById('box');
const animation = createAnimationTimer(
2000,
(progress) => {
element.style.transform = `translateX(${progress * 300}px)`;
},
() => {
console.log('Animation complete!');
}
);
animation.start();
function createRateLimiter(maxCalls, timeWindow) {
const calls = [];
return function(fn) {
const now = Date.now();
// Remove old calls outside time window
while (calls.length > 0 && now - calls[0] > timeWindow) {
calls.shift();
}
if (calls.length < maxCalls) {
calls.push(now);
return fn();
} else {
const oldestCall = calls[0];
const waitTime = timeWindow - (now - oldestCall);
return new Promise((resolve) => {
setTimeout(() => {
calls.shift();
calls.push(Date.now());
resolve(fn());
}, waitTime);
});
}
};
}
// Allow 3 calls per second
const limiter = createRateLimiter(3, 1000);
// Make multiple calls
for (let i = 0; i < 10; i++) {
limiter(() => {
console.log(`Call ${i} at ${Date.now()}`);
return fetch('/api/data');
});
}
let or create explicit closuresNow that you understand closures with timing functions, in the next lesson we'll explore how closures work with array methods like map, filter, and reduce.