What are JavaScript closures?

🕑 1 minute read

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:

let 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.

let 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.

let 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.

let 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.

let 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.

// 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.

function 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.