What are JavaScript closures?
May 28, 2020
Closures are a way to retain a local variable's privacy, but allow it to be accessible in a global scope.
JavaScript has global
variables and local
variables. Global variables are accessible to everything in your application. Local variables are only accessible within the local scope. For example:
1let runs = 1; function doSomething() { console.log(runs) // => 1 let cycles = 2; console.log(cycles) // => 2 } doSomething(); console.log(runs) // => 1 console.log(cycles) // => ReferenceError: cycles is not defined
As you can see cycles
only exists inside our function doSomething
because it is locally scoped to it.
What if we want to keep track of a value over time such as a counter? We could use a global value similar to runs
, so that everything that needs it can access it.
1let activities = 0; function addActivity() { activities += 1; // Add one } addActivity(); // => 1 addActivity(); // => 2 addActivity(); // => 3
However, this also means everything can change the variable and we could end up with it being overridden by accident in a large application.
1let activities = 0; function addActivity() { activities += 1; // Add one console.log(activities); } addActivity(); // => 1 addActivity(); // => 2 // ... much later and somewhere else function doSomthingWithActivities() { activities = 0; // Oops! Someone set the global variable back to 0! console.log(activities); } doSomthingWithActivities(); // => 0 addActivity(); // => 1, not 3!
The solution - closures.
We create a function (which contains it's own local scope). When we call this function it keeps it's scope self-contained and so we have isolated that variable from being changed.
We create a function and assign it to the addActivity
variable. In this case the function is self-invoking, so it will run as soon as we create it. In the function we create a variable activities
which is local to the function - Importantly any child function also has access to this scope... and then we return a child function that does the counting up.
1let addActivity = (function () { let activities = 0; // This is local to addActivity // return a function. This has access to the scope of addActivity, // so it can view and change activities return function () { // activities += 1; console.log(activities); } })(); addActivity(); // => 1 addActivity(); // => 2 activities = 1000; // Hahaha you can't do anything to my variable! addActivity(); // => 3
Note: The addActivity
function logs these values, but in reality you can just return the and use it.
1let latestActivityCount = addActivity(); // => 4
More complex
It would be annoying to have to make multiple similar functions for something like counting activities, so we can create a named closure function and the call it and assign it to a variable.
1// Create a named function function addActivity (type) { let activities = 0; return function () { activities += 1; console.log(`${type}: ${activities}`); } } // We can create a new closure as we need it // we can reuse this again and again if we want more types. let addRun = addActivity("run"); let addCycle = addActivity("cycle"); addRun() // => "run: 1" addCycle() // => "cycle: 1" addCycle() // => "cycle: 2" addRun() // => "run: 2" addRun() // => "run: 3"
Returning multiple functions or values
I our example we only have the updated value returned. That's ok, but what if we need to do other things to our value, such get the current value without changing it, or reset it to zero. Instead of returning a single function, we can just map multiple fucntions to an object and return that.
1function activity (type) { let activities = 0; // return an object { } return { add: function () { // we now have an add function we can use activities += 1; console.log(`${type}: ${activities}`); }, reset: function () { // we now have a reset function activities = 0; console.log(`${type}: ${activities}`); }, value: function () { // and a value function // It has to be a function to make it dynamic. // If we just returned activities it would be stuck at 0 return activities }, } } const runs = activity("run"); // We need to call the function by using it's key name runs.add() // "run: 1" runs.add() // "run: 2" runs.reset() // "run: 0" runs.add() // "run: 1" runs.add() // "run: 2" console.log(runs.value()) // "run: 2"
Privacy
JavaScript doesn't have private methods like some other languages. Because a closure encapsulates the scope it essentially creates a way to make variables and functions private. Nothing outside of the closure can change the activities
value unless it uses one of the returned functions. This is very powerful for being able to scope variables to keep the private and therefore protected for being changed by side effects.