Advanced JavaScript for Full Stack and Mobile Development
Closures and Scopes
Where Code Lives
In JavaScript, where you write your code matters. It's not just about organization; it determines how your functions access variables. This concept is called lexical scoping, or static scoping. The 'lexical' part means that a function's scope is determined by its position in the source code at the time it's written, not when it's called.
Think of it like nested boxes. A function can see any variables declared in its own box, as well as any variables in the boxes that contain it, all the way out to the global scope. However, an outer box cannot peer into an inner box.
function outer() {
let outerVar = 'I am outside!';
function inner() {
// inner() can access outerVar
console.log(outerVar);
}
inner();
}
outer(); // Logs: 'I am outside!'
Here, the inner function has access to outerVar simply because it's physically written inside the outer function. This relationship is locked in when the code is first parsed by the JavaScript engine.
Functions That Remember
Now, let's take lexical scoping a step further. What happens when an outer function finishes running, but we still have access to one of its inner functions? This is where we get a closure.
A closure is the combination of a function and the lexical environment within which that function was declared. In simpler terms, a function 'remembers' the variables from its birthplace, even after that birthplace is gone. It takes a little snapshot of its surrounding state and carries it around.
function createGreeter(greeting) {
return function(name) {
console.log(greeting + ', ' + name);
};
}
const sayHello = createGreeter('Hello');
sayHello('Alice'); // Logs: 'Hello, Alice'
const sayHi = createGreeter('Hi');
sayHi('Bob'); // Logs: 'Hi, Bob'
When createGreeter('Hello') is called, it does its job and returns the inner function. Normally, we'd expect the greeting variable to disappear. But because the inner function still needs it, JavaScript keeps that variable alive in a closure. The function assigned to sayHello remembers that its greeting is 'Hello'.
Closures are a fundamental concept in JavaScript that allow functions to access variables from an outer function even after the outer function has returned.
Building Functions on Demand
This ability to create pre-configured functions is incredibly powerful. The pattern we just saw is called a function factory. It’s a function that doesn’t do the final work itself, but instead manufactures other functions tailored to a specific task.
function createMultiplier(factor) {
return function(number) {
return number * factor;
};
}
// Create a function that always multiplies by 2
const double = createMultiplier(2);
// Create a function that always multiplies by 10
const timesTen = createMultiplier(10);
console.log(double(5)); // Logs: 10
console.log(timesTen(5)); // Logs: 50
Function factories help us write DRY (Don't Repeat Yourself) code. Instead of writing separate functions for double, triple, and so on, we have one factory that can generate any multiplier we need. Each created function is a closure, holding onto its unique factor.
Keeping Secrets
Closures are also the key to creating private variables in JavaScript. By default, any variable defined in an object is public. But with closures, we can create a private scope that protects data from outside interference. This is called encapsulation.
The most common way to do this is with the module pattern. An outer function defines some private variables and then returns an object containing methods. These methods have access to the private variables because of their closure, but the outside world does not.
function createCounter() {
let count = 0; // This variable is private
return {
increment: function() {
count++;
console.log(count);
},
decrement: function() {
count--;
console.log(count);
},
getCount: function() {
return count;
}
};
}
const counter = createCounter();
counter.increment(); // Logs: 1
counter.increment(); // Logs: 2
// You cannot access 'count' directly
console.log(counter.count); // Logs: undefined
In this example, the count variable is completely inaccessible from the global scope. We can only interact with it through the methods increment, decrement, and getCount. This prevents accidental changes to the state and makes our code more robust and predictable. This pattern is foundational to how many popular JavaScript libraries and frameworks manage their internal state.
Now that you understand how closures work, let's test your knowledge.
What does 'lexical scoping' mean in JavaScript?
Consider the following code snippet:
function outer() {
const outerVar = 'I am outside!';
function inner() {
const innerVar = 'I am inside!';
console.log(outerVar);
}
return inner;
}
const myFunc = outer();
myFunc();
What is logged to the console when myFunc() is executed?