JavaScript Closures Tutorial - Section 4: Advanced Concepts
Good news: Modern JavaScript engines optimize closures heavily. In most cases, the performance impact is negligible. However, understanding the costs helps you make informed decisions.
Each closure instance maintains its own lexical environment:
// Each counter has its own environment
function createCounter() {
let count = 0;
return () => ++count;
}
const counters = [];
for (let i = 0; i < 1000; i++) {
counters.push(createCounter());
}
// 1000 separate environments in memory
// Each with its own 'count' variable
// ❌ Each closure has its own copy
function createFormatter() {
const config = {
prefix: '[LOG]',
dateFormat: 'ISO',
colors: { info: 'blue', error: 'red' }
};
return (message) => {
return `${config.prefix} ${message}`;
};
}
// ✅ Share config across all closures
const sharedConfig = {
prefix: '[LOG]',
dateFormat: 'ISO',
colors: { info: 'blue', error: 'red' }
};
function createFormatter() {
return (message) => {
return `${sharedConfig.prefix} ${message}`;
};
}
// ❌ Creates new closure on every render
function Component() {
return (
);
}
// ✅ Reuse the same function
function handleClick() {
console.log('clicked');
}
function Component() {
return (
);
}
// ✅ Or use closure only when needed
function Component({ userId }) {
const handleClick = () => {
console.log('User:', userId);
};
return (
);
}
// ❌ Captures more than needed
function processData(largeArray) {
const result = largeArray.map(x => x * 2);
const sum = result.reduce((a, b) => a + b, 0);
return function() {
// Only needs sum, but captures largeArray and result too!
return sum;
};
}
// ✅ Only capture what's needed
function processData(largeArray) {
const result = largeArray.map(x => x * 2);
const sum = result.reduce((a, b) => a + b, 0);
// largeArray and result go out of scope here
return function() {
return sum; // Only captures sum
};
}
// ❌ Each instance has its own methods
function Counter(initial) {
let count = initial;
this.increment = function() {
count++;
return count;
};
this.decrement = function() {
count--;
return count;
};
}
// ✅ Share methods on prototype
function Counter(initial) {
this._count = initial;
}
Counter.prototype.increment = function() {
this._count++;
return this._count;
};
Counter.prototype.decrement = function() {
this._count--;
return this._count;
};
// Trade-off: No private variables, but better memory usage
// ❌ Computes immediately even if never used
function createExpensiveResource() {
const resource = computeExpensiveValue();
return {
getResource: () => resource
};
}
// ✅ Compute only when needed
function createExpensiveResource() {
let resource = null;
let computed = false;
return {
getResource: () => {
if (!computed) {
resource = computeExpensiveValue();
computed = true;
}
return resource;
}
};
}
// Benchmark closure vs direct access
function benchmarkClosure() {
let value = 0;
const withClosure = () => value++;
console.time('closure');
for (let i = 0; i < 1000000; i++) {
withClosure();
}
console.timeEnd('closure');
console.time('direct');
for (let i = 0; i < 1000000; i++) {
value++;
}
console.timeEnd('direct');
}
benchmarkClosure();
// Modern engines: difference is minimal!
// Optimized for performance
class EventManager {
constructor() {
this.listeners = new Map();
}
on(event, callback) {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event).add(callback);
// Return bound unsubscribe (closure only when needed)
return this.off.bind(this, event, callback);
}
off(event, callback) {
const callbacks = this.listeners.get(event);
if (callbacks) {
callbacks.delete(callback);
}
}
emit(event, data) {
const callbacks = this.listeners.get(event);
if (callbacks) {
// Avoid creating closure in loop
for (const callback of callbacks) {
callback(data);
}
}
}
}
const events = new EventManager();
const unsubscribe = events.on('data', console.log);
events.emit('data', 'test');
unsubscribe();
// Optimized memoization with size limit
function memoize(fn, maxSize = 100) {
const cache = new Map();
return function(...args) {
// Use simple key for single arg, JSON for multiple
const key = args.length === 1 ? args[0] : JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn.apply(this, args);
// Limit cache size
if (cache.size >= maxSize) {
const firstKey = cache.keys().next().value;
cache.delete(firstKey);
}
cache.set(key, result);
return result;
};
}
const memoizedFn = memoize(expensiveFunction, 50);
// Monitor function performance
function measurePerformance(fn, name) {
return function(...args) {
const start = performance.now();
const result = fn.apply(this, args);
const end = performance.now();
console.log(`${name} took ${(end - start).toFixed(2)}ms`);
return result;
};
}
const slowFunction = measurePerformance(
() => {
let sum = 0;
for (let i = 0; i < 1000000; i++) {
sum += i;
}
return sum;
},
'slowFunction'
);
slowFunction();
Modern JavaScript engines are incredibly good at optimizing closures. In most applications, closures are not a performance bottleneck. Focus on:
// Tip 1: Reuse closures when possible
const handlers = {
click: (e) => console.log('clicked', e),
hover: (e) => console.log('hovered', e)
};
elements.forEach(el => {
el.addEventListener('click', handlers.click);
el.addEventListener('mouseover', handlers.hover);
});
// Tip 2: Use WeakMap for automatic cleanup
const privateData = new WeakMap();
function createObject(data) {
const obj = {};
privateData.set(obj, data);
return obj;
}
// Tip 3: Batch operations
function batchProcess(items, batchSize = 100) {
const batches = [];
for (let i = 0; i < items.length; i += batchSize) {
batches.push(items.slice(i, i + batchSize));
}
return batches.map(batch =>
() => batch.forEach(processItem)
);
}
You've completed the JavaScript Closures tutorial! You now understand:
Closures are a fundamental JavaScript feature that enables powerful patterns. Use them wisely, and they'll make your code more elegant, maintainable, and expressive.
Keep practicing! The best way to master closures is to use them in real projects. Start small, experiment, and gradually incorporate these patterns into your code.