DEV Community

loading...

Some Closure on Closures

jckuhl profile image Jonathan Kuhl ・6 min read

Introduction

Closures are a concept that take a while for many new JavaScript developers to get used to. It's one of the more abstract concepts of JavaScript. A closure exists when you have a function within a function that has access to the outer function's state.

What?

See, that's the definition I always see when someone defines a closure. But it's not as clear cut as to what it really means, so let me explain

Execution Context

When a function is called, JavaScript's engine creates what is called an execution context. This context contains all the state required for that function. In simple terms, state is simply the variables and their current values.

function foo() {
    const a = 3;
}

In the function foo() above, when you call foo() an execution context is created, the variable a is set to 3 and then the function ends, the context is destroyed and the variable is destroyed and the function returns undefined.

This is also how scope works. a is declared in the function and only exists within that function and any lower blocks that may appear in that function.

Any internal functions within foo() can access foo()'s state.

function foo() {
    const a = 3;
    function log() {
        console.log(a);
    }
    log();
}

But this is just basic scoping right? Well yes, in this example, but here's what's powerful about closures. If the outer function foo() is destroyed, the internal log() function, if it was brought out of the function, would still have access to foo()'s state.

function foo() {
    const a = 3;
    function log() {
        console.log(a);
    }
    return log;
}

const log = foo();
log();    // logs 3

// foo()() would also be acceptable, and would also log 3

The internal log() function still has foo()'s execution context, even though foo() was called, created, and destroyed.

To illustrate this further, let's make foo() take a parameter rather than a hard coded variable.

function foo(a) {
    function log() {
        console.log(a);
    }
    return log;
}

const log3 = foo(3);
const log4 = foo(4);

log3();    //logs a '3'
log4();    //logs a '4'

// alternatively
foo('hello')();    //logs 'hello'

Here you can see foo() is called 3 times with different values for a and the returned function still "remembers" the value of a from the execution context.

That's essentially what a closure is. It's an internal function that has access to the the outer function's state.

But Why?

Why would I need to use this? Well, there are a number of situations where its useful to utilize closures. Generators use closures. Event handlers use closures. Partial application of functions use closures. Closures are a major component of functional programming.

Here's how you can create a generator in JavaScript. This one is similar to (but simpler than) Python's range() object:

This is a handmade generator by the way, rather than the built in generator syntax that comes with JavaScript. For more information on the official JavaScript generators, check out the Mozilla Developer Network

function range(start, end, step=1) {
    let count = 0;
    return () => {
        count += 1;
        if(start < end) {
            return start += count !== 1 ? step : 0;
        } else {
            return false;
        }
    }
}

const r = range(1, 5);
console.log(r());    // logs 1
console.log(r());    // logs 2
console.log(r());    // logs 3
console.log(r());    // logs 4
console.log(r());    // logs 5
console.log(r());    // logs false

The ternary count !== 1 ? step : 0; is used to ensure the range includes the start value

The range() function returns an anonymous function that keeps track of the current state of the three parameters passed into the function. Each time you call r(), it will return the next iteration of that state, which is mutated by the expression start += step. Starting with this range object, it's not terribly difficult to use closures to rewrite many of the JavaScript array functions to functional functions that work on generators instead.

Here's what map() might look like.

function map(mapping) {
    return (range)=> ()=> {
        const value = range();
        if(value && value !== false) {
            return mapping(value);
        } else {
            return false;
        }
    }
}

const squares = map(x => x ** 2)(range(1,5));
console.log(squares());    //logs 1
console.log(squares());    //logs 4
console.log(squares());    //logs 9
console.log(squares());    //logs 16
console.log(squares());    //logs 25
console.log(squares());    //logs false

Note that with arrow notation ()=> ()=> {} is what a closure would look like.

Here you have a generator to create square numbers. Each time the function is called, it "remembers" the execution context of the outer function.

You can, of course, loop over the generators as well.

let s;
while(s = squares()) {
    console.log(s);
}

Don't get confused with the expression in the while loop. That = is an assignment operator, not an equality operator. = is a function under the hood however, and it will return the value of squares(), which is either a number, or false, and thus we can take advantage of truthy/falsy here.

But I felt writing it out was clearer.

You can see the code for these generators in action at Repl.it

Saving State

Closures also work when you need to save state. Imagine you have a large app that needs to connect to multiple mongo databases. I have an express back end and I need to export multiple connection functions to multiple javascript files. A closure can be a simple way of doing this:

//in a file called "database.js"
const mongoose = require('mongoose');

const user = process.env.MONGO_USER;
const password = process.env.MONGO_PW;
const db1URI = `mongodb+srv://${user}:${password}@cluster1.mongodb.net/database1?retryWrites=true`;
const db2URI = `mongodb+srv://${user}:${password}@cluster2.mongodb.net/database2?retryWrites=true`;
const db3URI = `mongodb+srv://${user}:${password}@cluster3.mongodb.net/database3?retryWrites=true`;

// wrap the connection in a closure so I can export it with the URI
function Connect(uri) {
    return function() {
        mongoose.connect(uri, {
            auth: {
                user,
                password
            },
            useNewUrlParser: true
        });

        const db = mongoose.connection;
        db.on('error', console.error.bind(console, 'connection error'));
        db.once('open', ()=> {
            console.log('\nSuccessfully connected to Mongo!\n');
        });
    }
}

const db1Connect = Connect(db1URI);
const db2Connect = Connect(db2URI);
const db3Connect = Connect(db3URI);

module.exports = {
    db1Connect,
    db2Connect,
    db3Connect
};

Then in various modules in your Express code you could say

const MongooseConnect = require('./database.js');
MongooseConnect.db1Connect();

//and in another file somewhere else
const MongooseConnect = require('./database.js');
MongooseConnect.db2Connect();

//etc

Here the Connect() method saves the URI parameter passed in in a closure so that later when you actually call it, it can connect to Mongo (through Mongoose) with that URI. This allows me to have one single function for connecting and one central location with all the connection strings gathered in one spot. I could simply export a function and pass the string as a parameter, but then I'd have to define a connection string in different files that use the Connect() function or have an object defined in another file with all the connection strings in one spot. With a closure, I can simply export the functions and have all my connection strings in one spot where I can maintain them with ease.

Events

Closures also work with asynchronous operations and events. In fact, when you pass a callback to a click handler, that is by definition a closure. addEventListener is a function, the handler you pass into it would be the closure.

Here's a piece of code I wrote when it finally clicked how a closure works for me:

function clicker() {
    let counter = 0;
    const myDiv = document.getElementById("mydiv");
    const btn = document.querySelector("button");
    btn.addEventListener('click', ()=> {
        myDiv.innerHTML = counter;
        counter++;
    });
}

clicker();

I had a need to have the event listener added to a DOM element within a function and I wrote the above to make sure the concept itself worked. It's a simple counter, you click on a button the number goes up. Hurray, I guess.

But the point is, the anonymous click event handler still has access to the counter variable and the myDiv element, even though the clicker() function will already have its execution context destroyed by the time the user clicks the button (unless he's got a super fast millisecond reaction speed I suppose.) Even though counter and myDiv are scoped to clicker(), the event handler can still access them.

Asynchronous functions and events work just fine with closures because that closure still has access to the state of the enclosing function even if there's some time between the destruction of the enclosing function and the calling of the closure. If you have some closure that calls some network API and it takes 250 milliseconds to get a response, then that's fine, the closure still has access to the enclosing state.

Summary

Closures are tricky to understand. Hopefully some of the examples above made it clearer how they work. Remember, a closure is simply an internal function that has access to the state of the function that it is contained in. Here's an idea to get a better handle of closures, use the range() function I provided above and try to make a toArray() method that gives an array for each value in the generator. Or try to make a filter() function or rework any of the other JavaScript Array methods to work on range(). I've made a few on my own and they'll all require you to use closures.

Thank you and happy coding.

Discussion

pic
Editor guide
Collapse
cristiancota profile image
Cristian Cota

That's quite good explanation, thanks!

In the database example, you put:

const db1Connect = Connect(db1URI);
const db2Connect = Connect(db1URI);
const db3Connect = Connect(db1URI);

You may want to change it to:

const db1Connect = Connect(db1URI);
const db2Connect = Connect(db2URI);
const db3Connect = Connect(db3URI);
Collapse
jckuhl profile image