JavaScript Mastery and Application
Closures and Scoping
Where Variables Live
In JavaScript, where you declare a variable matters. This concept is called scope. When a function is defined, it doesn't just remember its own code; it also remembers the environment where it was created. This environment includes any variables that were available at that time. This is known as lexical scoping or static scoping.
Lexical scoping means that a variable's visibility is determined by its location within the source code during writing, not by where the function is called at runtime.
Think of it like nested boxes. A function can see variables in its own box and in any of the boxes that contain it, all the way out to the global scope. But an outer box can't see into an inner box.
// Global scope (the outermost box)
const outerVar = 'I am outside!';
function outerFunction() {
// outerFunction's scope (a smaller box inside global)
const innerVar = 'I am inside!';
function innerFunction() {
// innerFunction's scope (the smallest box)
console.log(outerVar); // 'I am outside!' - Can see outer boxes
console.log(innerVar); // 'I am inside!' - Can see its container
}
innerFunction();
}
outerFunction();
In this example, innerFunction has access to innerVar and outerVar because it's lexically nested within their scopes. This predictable behavior is the foundation for a powerful feature: closures.
Functions That Remember
A closure is when a function “remembers” variables from its outer scope, even after that outer function has finished executing.
This is where things get interesting. What happens if we return a function from another function? The returned function brings its original lexical environment with it, like a backpack full of variables from where it was born. This combination of a function and its captured environment is called a closure.
function createGreeter(greeting) {
// This outer function's scope contains 'greeting'.
function greet(name) {
// This inner function will 'remember' the greeting.
console.log(`${greeting}, ${name}!`);
}
return greet; // Return the inner function.
}
// createGreeter executes and returns.
// The 'greeting' variable should be gone, right?
const sayHello = createGreeter('Hello');
const sayHola = createGreeter('Hola');
sayHello('Alice'); // Logs: 'Hello, Alice!'
sayHola('Bob'); // Logs: 'Hola, Bob!'
Even though the createGreeter function has already finished running, the sayHello and sayHola functions still have access to the greeting variable they were created with. This is a [{createGreeter creates a new, separate scope, so sayHello and sayHola have their own private greeting variable locked away.
Practical Patterns
Closures aren't just a neat trick; they enable powerful design patterns for writing cleaner, more organized code. One of the most common is creating private variables.
Unlike languages like Java or C++, JavaScript doesn't have a built-in
privatekeyword for object properties. We can simulate this privacy using closures.
The Module Pattern uses a closure to create a public API while keeping the implementation details hidden. This is done by wrapping a set of variables and functions in an Immediately Invoked Function Expression (IIFE) and returning an object with only the methods you want to expose.
const counter = (function() {
let privateCount = 0; // This is a private variable
function changeBy(val) {
privateCount += val;
}
// The returned object is our public API
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCount;
}
};
})();
console.log(counter.value()); // 0
counter.increment();
counter.increment();
console.log(counter.value()); // 2
console.log(counter.privateCount); // undefined (It's private!)
The privateCount variable can only be accessed and modified by the methods inside the closure. This prevents accidental changes and makes the component's interface clear and predictable.
Another powerful use is creating function factories—functions that generate and configure other functions. The createGreeter example from earlier was a simple function factory. They are incredibly useful for creating customized functions on the fly, reducing code duplication.
Memory Considerations
Because closures hold references to their outer scope variables, those variables are kept in memory. They won't be cleaned up by JavaScript's as long as the inner function that uses them still exists. In most cases, this is exactly what you want. However, it's something to be aware of. If you create many closures that hold references to large objects, it can lead to higher memory usage.
When the reference to the inner function is removed (for example, by setting sayHello = null), and if no other references exist, both the closure and its captured environment become eligible for garbage collection, freeing up the memory.
Let's review these core ideas.
Now, test your understanding.
What is the term for JavaScript's behavior where a function remembers the environment (the scope) in which it was created, regardless of where it is later executed?
Consider the following code snippet. What will be logged to the console?
function createGreeter(greeting) {
return function(name) {
console.log(greeting + ', ' + name);
};
}
const sayHello = createGreeter('Hello');
sayHello('Alice');
Understanding lexical scoping and closures is a major step. It unlocks patterns that lead to more modular, maintainable, and secure JavaScript code.