JavaScript Closures Tutorial - Section 4: Advanced Concepts
Closures keep their lexical environment alive in memory. This is powerful but can lead to memory leaks if not managed properly.
function createClosure() {
const largeData = new Array(1000000).fill('data');
return function() {
// This closure keeps largeData in memory
return largeData.length;
};
}
const closure = createClosure();
// largeData is still in memory even though createClosure finished!
// ❌ Memory leak: Event listener never removed
function setupButton() {
const largeData = new Array(1000000).fill('data');
document.getElementById('myButton').addEventListener('click', function() {
console.log(largeData.length);
});
}
setupButton();
// largeData stays in memory forever!
// ✅ Solution: Remove event listener when done
function setupButton() {
const largeData = new Array(1000000).fill('data');
const button = document.getElementById('myButton');
const handler = function() {
console.log(largeData.length);
};
button.addEventListener('click', handler);
// Return cleanup function
return function cleanup() {
button.removeEventListener('click', handler);
};
}
const cleanup = setupButton();
// Later, when done:
cleanup(); // Now largeData can be garbage collected
// ❌ Memory leak: Timer never cleared
function startPolling() {
const largeData = new Array(1000000).fill('data');
setInterval(function() {
console.log(largeData.length);
}, 1000);
}
startPolling();
// largeData and interval run forever!
// ✅ Solution: Return cleanup function
function startPolling() {
const largeData = new Array(1000000).fill('data');
const intervalId = setInterval(function() {
console.log(largeData.length);
}, 1000);
return function stopPolling() {
clearInterval(intervalId);
};
}
const stop = startPolling();
// Later:
stop(); // Clears interval and allows garbage collection
// ❌ Potential memory leak: Circular reference
function createCircular() {
const obj1 = {};
const obj2 = {};
obj1.ref = obj2;
obj2.ref = obj1;
return function() {
return obj1;
};
}
// Modern JavaScript engines handle this, but be aware
// ✅ Better: Break references when done
function createCircular() {
let obj1 = {};
let obj2 = {};
obj1.ref = obj2;
obj2.ref = obj1;
return {
get: function() {
return obj1;
},
cleanup: function() {
obj1.ref = null;
obj2.ref = null;
obj1 = null;
obj2 = null;
}
};
}
Always provide a way to clean up closures:
function createSubscription(topic, callback) {
const listeners = [];
// Subscribe
listeners.push(callback);
// Return unsubscribe function
return function unsubscribe() {
const index = listeners.indexOf(callback);
if (index > -1) {
listeners.splice(index, 1);
}
};
}
const unsubscribe = createSubscription('news', (data) => {
console.log(data);
});
// When done:
unsubscribe(); // Allows garbage collection
Use WeakMap to allow garbage collection:
// Using WeakMap allows garbage collection
const privateData = new WeakMap();
function createObject(data) {
const obj = {};
// Store private data
privateData.set(obj, data);
return {
getData: function() {
return privateData.get(obj);
}
};
}
let myObj = createObject({ secret: 'value' });
// When myObj is no longer referenced, both it and its
// private data can be garbage collected
myObj = null; // Private data can now be collected
function createCache() {
let cache = new Map();
let isActive = true;
return {
set: function(key, value) {
if (isActive) {
cache.set(key, value);
}
},
get: function(key) {
if (isActive) {
return cache.get(key);
}
},
destroy: function() {
cache.clear();
cache = null; // Allow garbage collection
isActive = false;
}
};
}
const cache = createCache();
cache.set('key', 'value');
// When done:
cache.destroy(); // Cleans up and allows GC
// Monitor memory usage
function monitorMemory() {
if (performance.memory) {
console.log({
usedJSHeapSize: (performance.memory.usedJSHeapSize / 1048576).toFixed(2) + ' MB',
totalJSHeapSize: (performance.memory.totalJSHeapSize / 1048576).toFixed(2) + ' MB',
jsHeapSizeLimit: (performance.memory.jsHeapSizeLimit / 1048576).toFixed(2) + ' MB'
});
}
}
// Check before and after operations
monitorMemory();
// ... perform operations ...
monitorMemory();
// ❌ Captures unnecessary data
function createHandler() {
const largeData = loadLargeData();
const smallData = extractSmallData(largeData);
return function() {
// Only needs smallData, but captures largeData too!
return smallData;
};
}
// ✅ Better: Limit what's captured
function createHandler() {
const largeData = loadLargeData();
const smallData = extractSmallData(largeData);
// largeData goes out of scope here
return function() {
return smallData; // Only captures smallData
};
}
// Use WeakMap/WeakSet for object keys
const cache = new WeakMap();
function cacheResult(obj, result) {
cache.set(obj, result);
}
let myObj = { id: 1 };
cacheResult(myObj, 'result');
// When myObj is no longer referenced, it and its
// cached result can be garbage collected
myObj = null;
class ResourceManager {
constructor() {
this.resources = [];
this.isDisposed = false;
}
addResource(resource) {
if (this.isDisposed) {
throw new Error('Manager is disposed');
}
this.resources.push(resource);
}
dispose() {
if (this.isDisposed) return;
// Clean up all resources
this.resources.forEach(resource => {
if (resource.cleanup) {
resource.cleanup();
}
});
this.resources = [];
this.isDisposed = true;
}
}
const manager = new ResourceManager();
// ... use manager ...
manager.dispose(); // Clean up when done
// ❌ Accidental global
function createClosure() {
// Missing 'const/let/var' creates global!
data = new Array(1000000);
return function() {
return data.length;
};
}
// ✅ Always declare variables
function createClosure() {
const data = new Array(1000000);
return function() {
return data.length;
};
}
function createEventManager() {
const listeners = new Map();
return {
on: function(event, callback) {
if (!listeners.has(event)) {
listeners.set(event, new Set());
}
listeners.get(event).add(callback);
// Return unsubscribe function
return () => {
const callbacks = listeners.get(event);
if (callbacks) {
callbacks.delete(callback);
if (callbacks.size === 0) {
listeners.delete(event);
}
}
};
},
emit: function(event, data) {
const callbacks = listeners.get(event);
if (callbacks) {
callbacks.forEach(callback => callback(data));
}
},
removeAllListeners: function(event) {
if (event) {
listeners.delete(event);
} else {
listeners.clear();
}
},
destroy: function() {
listeners.clear();
}
};
}
const events = createEventManager();
const unsubscribe = events.on('data', (data) => console.log(data));
// Clean up when done
unsubscribe();
// Or destroy everything
events.destroy();
function createComponent(element) {
const cleanupFunctions = [];
let isDestroyed = false;
function registerCleanup(fn) {
cleanupFunctions.push(fn);
}
// Setup event listeners
const clickHandler = () => console.log('clicked');
element.addEventListener('click', clickHandler);
registerCleanup(() => {
element.removeEventListener('click', clickHandler);
});
// Setup interval
const intervalId = setInterval(() => {
console.log('tick');
}, 1000);
registerCleanup(() => {
clearInterval(intervalId);
});
return {
destroy: function() {
if (isDestroyed) return;
// Run all cleanup functions
cleanupFunctions.forEach(fn => fn());
cleanupFunctions.length = 0;
isDestroyed = true;
}
};
}
const component = createComponent(document.getElementById('myElement'));
// When component is no longer needed:
component.destroy();
Now that you understand memory management with closures, in the final lesson we'll explore performance considerations and optimization techniques.