What is scope and closure in JavaScript?
You can think of scope in JavaScript as a semitransparent bag containing names of variables and functions. While you are in the bag you can clearly see what is outside of it, but nobody from the outside can look into your bag.
Scope and closure are two of the core building blocks of JavaScript, making it very powerful. The idea is quite simple, although my experience of interviewing candidates for JavaScript developer roles shows that it is often misunderstood. There has been a lot, in-depth said about scope and closure already. I don't want to repeat all of that in yet another article. Instead, I would like to take slightly different approach - show the most important characteristics in an easy to digest form with very simple examples.
Function scope and nesting
When you create a function, that function gets its own scope. As easy as that. Scope is the function's private bag of names on which it operates. Very important is "semitransparency" of the bag - the function operating in the bag can look outside, but nobody from the outside can look into the function's bag. Thus the scope is private to the function and functions nested in it.
// Code in the global scope has access to variable `g`
// but not `o` nor `i`.
var g = 'global';
function outer () {
// Code in this function scope has access to variables
// `o` and `g`
// but not `i`.
var o = 'outer';
function inner () {
// Code in this function scope has access to variables
// `i`, `o` and `g`.
var i = 'inner';
}
}
Another important thing to mention is that each scope you create is already nested within the global scope, which is the outermost bag. Therefore easy access to global variables can be considered as an extrapolation of the nested scope characteristic.
Clean scope on each run
Every time you run a function it gets its own scope in a clean, pristine state. None of the values from its previous executions are remembered, everything is initialised afresh. Therefore there is no state retained between subsequent executions of a function:
function count() {
var counter = 0;
counter += 1;
return counter;
}
console.log(count()); // 1
console.log(count()); // 1
console.log(count()); // 1
Retaining state
Now, let's consider for a brief moment, that the counter from the above example was declared on the global scope (it's bad, we know, bear with me here). Then you could actually retain state between subsequent function runs:
var counter = 0;
function count() {
counter += 1;
return counter;
}
console.log(count()); // 1
console.log(count()); // 2
console.log(count()); // 3
What's so cool about it? Well, not much. Using global scope for state is a really bad idea. However, as we said before, scope nesting would work the same way with an outer function. How about we try that. Let's create a countWrapper
and see what happens:
function countWrapper() {
var counter = 0;
console.log(count()); // 1
console.log(count()); // 2
console.log(count()); // 3
function count() {
counter += 1;
return counter;
}
}
countWrapper();
This time the state was retained between count
calls. We're getting somewhere. Not far yet, but we're taking baby steps here. It should be quite obvious by now, that if you called countWrapper
again you would get the same result as you got the first time. The counterWrapper
function would get clean scope, counter
would be assigned 0 again and in the console you would again get 1, 2 and 3.
That means, as long as the countWrapper
's scope lives, the state is retained. The counterWrapper
's scope dies and we are left with nothing. Not very handy. Here is the big question then - is there a way to preserve counterWrapper
's scope? And here is the big answer:
Closure
You can think of closure as a state retained between function calls. This state is created by using variables from function's outer scope.
Now, imagine that countWrapper
instead of calling count
- returns it, so that it can be called by someone else. Because functions are first class citizens in JavaScipt, you can pass them around. Now the question is - what would happen to the count
variable declared in countWrapper
? Shouldn't it disappear once countWrapper
finished execution? The returned function would be useless then. Let's see:
function countWrapper() {
var counter = 0;
return function count() {
counter += 1;
return counter;
}
}
var count = countWrapper();
console.log(count()); // 1
console.log(count()); // 2
console.log(count()); // 3
It turns out we're in luck! Returned function count
holds a reference to the counter
variable from its outer scope. And as long as there is a reference, garbage collector won't touch it. And so the counter
variable is retained between function calls. We managed to retain the state and at the same time we created closure.
Why is closure important?
Many a time there is a need to retain state between function calls even if is not being mutated. Primary example here is an event handler which needs to call some other function. In more general terms closure makes higher order functions possible and opens up JavaScript to the world of functional programming. It is very powerful.
Let me finish off with closure application which we almost covered without calling it by the proper name.
Factory
The countWrapper
from the previous example should be really called countFactory
, but I wanted to keep focus on closure. Let's rename it now and consider for a moment what happens, when we call countFactory
a few times. Well, we'll get a few counter
functions, right? Will they share the closure? In order to answer that let's through the "clean scope on each run" principle again. Each time function is called, it gets its new, fresh scope. That means, each time we call countFactory
we create new counter
and new count
function independent of the others. Let's test that theory:
function countFactory() {
var counter = 0;
return function count() {
counter += 1;
return counter;
}
}
var countA = countFactory();
var countB = countFactory();
console.log(countA()); // 1
console.log(countA()); // 2
console.log(countB()); // 1
console.log(countB()); // 2
There we go. This is a factory. Now we can go fancy and initialise the counter when we create the count
function. How would we do that? Parameters passed to a function become variables in function's scope. Therefore if we pass initial counter value to countFactory
we should get exactly what we want. Let's see:
function countFactory(counter) {
return function count() {
counter += 1;
return counter;
}
}
var countA = countFactory(0);
var countB = countFactory(10);
console.log(countA()); // 1
console.log(countA()); // 2
console.log(countB()); // 11
console.log(countB()); // 12
Summary
Let's quickly recap:
- Scope is like a semitransparent bag - sitting in it you can see what's outside,
nobody from out there can see what's inside. - Each function creates its own scope.
- There is also global scope, which is the outermost bag and all function scopes are nested within it. Therefore all functions have access to the global scope.
- When there is an inner function nested inside an outer function, the inner function can access variables from outer function's scope. It does not work the other way round.
- On each call a function gets new, clean scope, so there is no state preserved.
- Closure on the other hand is a state preserved between function calls, created by referencing variables from function's outer scope.
Read more
- Kyle Simpson's scope and closures from the great series "You don't know JS". He goes much deeper.
- MDN's Functions
- MDN's Closures
I hope you found my article useful.
Please remember to share if you liked it!
Jacek