Igor Šarčević wrote this on November 18, 2015
Closures are not magic
Several years ago, while I was still a high school student, a friend of mine introduced me the concept of closures. I didn’t understand even a bit of what he wanted to show me, but he looked really hyped when he talked about them. To me, it all looked like some kind of deep magic. Even Google didn’t help. All I could find were scientific papers, that were incomprehensible for a high school student.
I giggle a bit when I think back at my high school programming struggles. This article is an attempt to explain closures in simple terms, that would help my old high school self to easily grasp their power.
Counting events
We’ll start with a simple problem, that can be easily solved by introducing closures in our program.
Let’s say that we want to create a mechanisms for counting events. Such a mechanism would help us to keep track of our code’s execution, or even while trying to debug some issues. For example, we could use it like in the following example:
increment(); // Number of events: 1 increment(); // Number of events: 2 increment(); // Number of events: 3
As you can see in the above example, our desired code would display a message
“Number of events: x” every time we execute the increment()
function. Here is
a simple attempt to write such a function:
var counter = 0; function increment() { counter = counter + 1; console.log("Number of events: " + counter); }
Multiple counters
The above code is pretty straightforward. However, we quickly run into an issue if we want to introduce a second counter. Of course, we can implement two separate counter mechanisms, like in the following example, but it is obvious that something can be improved:
var counter1 = 0; function incrementCounter1() { counter1 = counter1 + 1; console.log("Number of events: " + counter1); } var counter2 = 0; function incrementCounter2() { counter2 = counter2 + 1; console.log("Number of events: " + counter2); } incrementCounter1(); // Number of events: 1 incrementCounter2(); // Number of events: 1 incrementCounter1(); // Number of events: 2
The above code screams of unnecessary duplication. Obviously, this solution won’t scale to more than two or three counters. We need to figure out something better.
Introducing our first closure
What we would really like in the above example, is to somehow introduce new counters, bundled with a function that can increase them without duplicating lots of code. Here is such an attempt using closures:
function createCounter() { var counter = 0; function increment() { counter = counter + 1; console.log("Number of events: " + counter); } return increment; }
Let’s see how this works. We will create two counters, and use them to track two independent events:
var counter1 = createCounter(); var counter2 = createCounter(); counter1(); // Number of events: 1 counter1(); // Number of events: 2 counter2(); // Number of events: 1 counter1(); // Number of events: 3
Ugh, that looks complicated… However, it is actually really simple. We just need to separate the implementation logic into small, easily digestible chunks. Let’s see what our implementation does:
First, it creates a local variable named
counter
.Secondly, it creates a local function named
increment
that can increment thecounter
variable. This can be pretty strange, if you have never worked with functional languages that treat functions as data. However, it is perfectly normal, and it only takes a little practice to get used to this idea.
Please notice that at this point, the implementation of createCounter()
looks
almost exactly like our original implementation of a counter. The only
difference is the fact that it is wrapped, or enclosed, in a function’s body.
Hence, these constructs are called closures.
Now comes the tricky part:
- The last step in
createCounter()
returns the localincrement
function. Please notice, that it does not return the result of calling the function, but the function itself.
That means, when we create new counters with the snippet bellow, we are actually generating new functions.
// fancyNewCounter is a function in this scope var fancyNewCounter = createCounter();
This is were the power of closures lie. Every function generated with
createCounter()
keeps track of their own generated counter
value.
In a sense, the returned function remembers the environment that it was
created in.
An important thing should be observed. The internal counter
variables are
independent of each other! For example, if we create two counters, each of
them will allocate a new counter
variable in the body of the closure. We can
observe two things:
Each counter will start counting from number 1
:
var counter1 = createCounter(); counter1(); // Number of events: 1 counter1(); // Number of events: 2 var counter2 = createCounter(); counter2(); // Number of events: 1
The second counter does not interfere with the value of the first counter:
counter1(); // Number of events: 3
Naming our counters
The message “Number of events: x” is OK, but it would be more helpful if the message could describe the type of the event we are counting. For example, it would be nice if we could add a name to our counters, like in the following example:
var catCounter = createCounter("cats"); var dogCounter = createCounter("dogs"); catCounter(); // Number of cats: 1 catCounter(); // Number of cats: 2 dogCounter(); // Number of dogs: 1
We can achieve this simply by passing an argument to our closures.
function createCounter(counterName) { var counter = 0; function increment() { counter = counter + 1; console.log("Number of " + counterName + ": " + counter); } return increment; }
Nice! Please notice an interesting behavior in the above createCounter()
implementation. Not only does it remembers the value of the local counter
variable, but also the value of the passed arguments.
Improving the public interface
When I say public interface, I mean how we use the counters. It is not intuitive that the created counter is a function that will increment its value when we execute it. The following would be much simpler:
var dogCounter = createCounter("dogs"); dogCounter.increment(); // Number of dogs: 1
Let’s create such an implementation:
function createCounter(counterName) { var counter = 0; function increment() { counter = counter + 1; console.log("Number of " + counterName + ": " + counter); }; return { increment : increment }; }
In the above code snippet, we simply return an object that contains all the functions in our closure. In a sense, we are defining a set of messages that our closure can respond to.
Adding a decrement
Now, we can very simply introduce a decrement function to our counter.
function createCounter(counterName) { var counter = 0; function increment() { counter = counter + 1; console.log("Number of " + counterName + ": " + counter); }; function decrement() { counter = counter - 1; console.log("Number of " + counterName + ": " + counter); }; return { increment : increment, decrement : decrement }; } var dogsCounter = createCounter("dogs"); dogsCounter.increment(); // Number of dogs: 1 dogsCounter.increment(); // Number of dogs: 2 dogsCounter.decrement(); // Number of dogs: 1
Hidden counter actions
The above code has two redundant lines. Yes, the ones with the console.log
. It
would be nice to create a function explicitly for displaying the value of the
counter. Let’s call such a function display
.
function createCounter(counterName) { var counter = 0; function display() { console.log("Number of " + counterName + ": " + counter); } function increment() { counter = counter + 1; display(); }; function decrement() { counter = counter - 1; display(); }; return { increment : increment, decrement : decrement }; } var dogsCounter = createCounter("dogs"); dogsCounter.increment(); // Number of dogs: 1 dogsCounter.increment(); // Number of dogs: 2 dogsCounter.decrement(); // Number of dogs: 1
It looks very similar to the increment()
and decrement()
functions, yet it
is very different. We didn’t return it in the resulting object! That means that
the following will fail:
var dogsCounter = createCounter("dogs"); dogsCounter.display(); // ERROR !!!
We made the display()
function hidden from the outside world. It is
only available from inside of createCounter()
.
Abstract data types
As you can see, we can very easily introduce abstract data types with closures. For example, let’s create an implementation for a stack using closures.
function createStack() { var elements = []; return { push: function(el) { elements.unshift(el); }, pop: function() { return elements.shift(); } }; } var stack = createStack(); stack.push(3); stack.push(4); stack.pop(); // 4
Note: Closures are probably not the best implementation for a stack data type in JavaScript. Prototypes are probably more memory friendly.
Closures and OOP
If you have done any object oriented programming in your life, you will probably notice that the above constructs look very similar to classes, objects, instance variables and private/public methods.
A closure, similarly like a class, associates some data with a couple of functions that operate on them. Consequently, you can use a closure anywhere that you might normally use an object.
Final words
Closures are an awesome property of a programming language. They can come very handy in situations when we want to create “true” hidden fields in JavaScript, or need to create very simple constructs, that would be considered an overkill for a class.