JavaScript Essentials and the DOM
Use modern JavaScript to make pages interactive and manipulate the DOM safely and efficiently.
Content
Functions and closures
Versions:
Watch & Learn
AI-discovered learning video
Functions and Closures — The Secret Sauce of JavaScript
You already know how variables and scoping work from the previous section. Closures are what happens when functions get memory and emotional baggage — they remember the environment they were created in.
Why this matters (and why your callbacks care)
If variables and scoping are the rules of the road, closures are the cars that carry state around. They let a function keep access to variables from the scope where it was created even after that scope has finished running. This is why closures are everywhere in web programming: event handlers, callbacks, modules, factories, debouncing, and basically any time you want to remember something.
This builds naturally on the scoping rules you learned earlier: lexical scope — functions capture the variables in their surrounding (lexical) environment at creation time.
Quick practical note: closures are incredibly useful for managing client-side state (like UI flags), but they are not a security mechanism for secrets — anything stored in browser JS is accessible to the user or to injected scripts.
Basic example: a function that remembers
Micro explanation
A factory produces functions that hold onto private variables.
function makeCounter() {
let count = 0; // private variable
return function() {
count += 1;
return count;
}
}
const c = makeCounter();
console.log(c()); // 1
console.log(c()); // 2
The returned function forms a closure over the count variable. Even though makeCounter() finished executing, count persists as long as the returned function exists.
Common pitfalls (and party tricks)
1) Loop + var classic bug
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // prints 3, 3, 3
}, 0);
}
Why? var is function-scoped; each callback shares the same i. They run later, so i has already become 3.
Fixes:
- Use
let(block-scoped) so each iteration gets its owni. - Or create an IIFE to capture the value.
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // 0, 1, 2
}
// Or old-school:
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(function() { console.log(j); }, 0);
})(i);
}
2) Memory leaks with DOM references
If a closure references a DOM node and you forget to remove listeners, the node may hang around in memory longer than expected:
function attach() {
const node = document.getElementById('big');
node.addEventListener('click', () => {
console.log('clicked', node); // closure keeps `node` alive
});
}
If you remove the node from the DOM but don't remove the listener, garbage collection can't free it — because the closure still references it. Always remove event listeners when nodes are no longer needed.
Real-world patterns using closures
Module pattern (private state)
const CounterModule = (function() {
let count = 0; // private
return {
inc() { return ++count; },
get() { return count; }
};
})();
CounterModule.inc();
console.log(CounterModule.get());
This is a simple way to create encapsulated state without exposing internals — very useful for UI components in the DOM (but remember: client-side encapsulation ≠ security).
Debounce using closures
A debounce function keeps a timer variable in a closure to delay action until the noise stops:
function debounce(fn, delay = 300) {
let timer = null;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
const onResize = debounce(() => console.log('resize!'), 200);
window.addEventListener('resize', onResize);
Factories / Currying
Closures make currying and specialized functions easy:
function multiply(a) {
return function(b) {
return a * b; // remembers a
}
}
const double = multiply(2);
console.log(double(5)); // 10
Closures and asynchronous code
Closures are vital with async callbacks, Promises, and event handlers because they allow callbacks to access context from where they were declared. Remember the loop/var bug above — it's an async timing issue combined with scoping.
When handling authenticated state (from the previous "State, Sessions, and Authentication" topic), you might use closures to keep UI state or cancel tokens for network requests. But do not rely on closures to hide tokens — any token stored in JS can be read from devtools or exploited by XSS.
Guidelines and best practices
- Use closures to encapsulate behavior and local state (modules, factories, debounce).
- Prefer
let/constto avoid classicvarclosure bugs. - Remove event listeners to prevent memory leaks when closures reference DOM nodes.
- Avoid storing sensitive secrets solely in client-side variables — closures are not a security boundary.
- Keep closures small and predictable — large closures can unintentionally keep many variables alive.
Quick summary: the emotional arc of a closure
- You create a function.
- It takes a snapshot of the variables around it.
- Later, when it runs, it still has access to that snapshot.
"This is the moment where the concept finally clicks." Closures let functions carry baggage — useful baggage like counters and timers, dangerous baggage like leaked DOM nodes or mistaken loop variables.
If you're building UI that handles authentication state (recall our session/auth topic), closures are a great way to maintain component state or cancel inflight requests, but remember: treat client-side state as ephemeral and discoverable.
Final nugget (because you deserve a neat trick)
Want a private method and a public API? Use a revealing module via an IIFE:
const AuthWidget = (function() {
let token = null; // private
function setToken(t) { token = t; }
function getToken() { return token; }
function logout() { token = null; /* remove listeners, cleanup */ }
return { setToken, getToken, logout };
})();
This pattern gives you the convenience of closure-based privacy plus a clean public interface.
Go forth and close those functions — but not your mind. Closures will make your code both more powerful and more mysterious. Embrace them, but keep an eye on DOM leaks and security boundaries.
Key takeaways
- Closures = function + remembered lexical environment.
- Use them for encapsulation (modules, factories), debouncing, currying.
- Beware: var + async = classic bugs; closures can cause memory leaks; closures are not security.
Happy closing. And when in doubt, use let and remove your event listeners.
Comments (0)
Please sign in to leave a comment.
No comments yet. Be the first to comment!