JavaScript Scope and Closures
Closures and scope are fundamental parts of JavaScript but often poorly explained and therefore the source of confusion to many developers. This is my take on explaining them to someone without a formal computer science background.
Before we begin, it’s worth noting that closures are really, really important. Many super-smart JavaScript experts think they’re literally the best idea ever in computer science (see Douglas Crockford, Kyle Simpson, etc). And they’re used everywhere so if you plan on writing JavaScript, you need to master this concept.
Scope
Scope is how a computer keeps track of all the variables in a program. It refers to the specific environment where a variable is accessible and can be used. JavaScript uses the lexical scoping approach which allows for scopes to be nested and therefore an outer scope encloses (hence closure) an inner scope. More on this soon!
There are two types of scope: global scope and local scope.
Global scope
If you declare a variable outside of any function or curly brackets {}
then it is in the global scope.
var greeting = "Hello world!"; // global scope
A variable in the global scope is accessible anywhere in your program.
var greeting = "Hello world!"; // global scope
function myFunc() {
console.log(greeting);
}
myFunc(); // "Hello world"
Think for a second about what happened. Our function myFunc
had no reference to a variable called greeting
, yet it was able to find greeting
because it was declared in the global scope.
However, you should generally avoid declaring variables in the global scope because it can lead to naming confusion. Consider what would happen if you created a global variable greeting
in your code and then months later someone else on your team created another global variable greeting
. Which one would the computer use? Since programs can be quite large in size, it’s very easy to fall into this trap.
In summary, global scope means a variable is always available but try to avoid using it!
Local scope
In contrast to global scope, a variable available only in a specific part of your code is known as a local variable. It exists in a local scope. There are two ways to create local scope in JavaScript: function scope and block scope.
Function scope
Whenever you declare a variable within a function, that variable is only available within that function. Access to the variable is confined to the function’s local scope.
function sayHi() {
var greeting = "Good morning";
console.log(greeting);
}
sayHi(); // "Good morning"
greeting; // ReferenceError: greeting is not defined
Block scope
When you declare a variable between curly brackets {}
with const
or let
it is only available within the block of those brackets. Hence the name block scope.
{
let firstName = "William";
var lastName = "Vincent"; // global scope!
console.log(firstName + ' ' + lastName); // "William Vincent"
}
console.log(firstName); // ReferenceError: firstName is not defined
console.log(lastName); // "Vincent"
Note that when using var
scope is only function-level. Even though we declared the variable lastName
within a block, because we used var
it was still declared in the global scope, not a block-level scope.
Separate function scope
Functions declared separately do not have access to each other’s scope. In the example below, we pass in the function myName
and then try to access its local-scoped variable firstName
.
function myName() {
var firstName = "William";
}
function sayMyName() {
myName();
console.log(firstName);
}
sayMyName(); // ReferenceError: firstName is not defined
It doesn’t work. The local scope of each function is completely separate.
Closures and nested scope
However when a function is defined within another function the inner function has access to the variable scope of the outer function. This nesting of functions also results in a nesting of scope. The outer scope is said to “enclose” (hence the term closure) the scope of the inner function.
function outer() {
var name = 'William';
function inner() {
console.log(name);
}
inner();
}
outer(); // "William"
This works! If you understand why, then you’ll understand closures.
There are two functions here, outer
and inner
, and therefore two separate scopes for our variables. But when the inner
function is invoked and tries to find name
it’s nowhere to be found. So what does it do? It goes up a level in scope to the outer
function and asks, Do you have a variable name
?
The answer is yes! So the outer variable name
is assigned by the compiler to the inner variable name
.
The key point is that when we have inner nested functions and therefore nested scopes, the computer doesn’t give up when it can’t find a local variable name. Instead it moves up a level to the enclosing function scope and asks the question again. This one-way process continues until we reach the global, top-most function scope. If at that point we can’t find a variable, then a ReferenceError is thrown.
Closures in the wild
Once you understand what a closure is and how it works, you’ll realize that they’re everywhere in JavaScript code.
Have you ever heard of an IIFE (Immediately Invoked Function Expression)? It uses closures to prevent pollution of the global scope and is used in popular libraries such as jQuery.
Heard of the “module pattern” which allows public and private methods? It’s used everywhere too.
And if you’ve ever interviewed for a JavaScript position, it’s likely there’s been a question on timers or click handlers that really is a question about closures and scope. Consider the code below. What is the output?
for (var i=1; i<=5; i++) {
setTimeout(function(){
console.log("i: " + i);
},i*1000);
}
You’re probably thinking 1 2 3 4 5
in one second increments, right? But really it’s 6 6 6 6 6
! The reason is closures!
There are two functions in this example: the setTimeout() method from the window object and within it our internal anonymous function with the console.log
statement. That means we have 3 nested scopes: the global scope, the setTimeout()
scope, and then the anonymous function scope. When the anonymous function asks for the variable i
in its local scope, it can’t find it. So it goes up a level to the setTimeout()
function, which also doesn’t have a reference. Finally it looks in the global scope and does find a declared variable i
, whose value is 6. There is only one i
here, the global one at the end of the for-loop.
How do you fix this? One way is with an IIFE. In this example, the setTimeout
function closes over line 2 (function(i)...)
rather than the outer for-loop.
for (var i=1; i<=5; i++) {
(function(i){
setTimeout(function(){
console.log("i: " + i);
}, i*1000);
})(i);
} // 1 2 3 4 5
Or you can use let
since it’s block scoped and therefore there will be a new i
on each iteration:
for (let i=1; i<=5; i++) {
setTimeout(function(){
console.log("i: " + i);
}, i*1000);
}
// 1 2 3 4 5
Conclusion
Each time we create a new function, we also create a new scope. And since functions can be nested within one another, so too can our scope. The JavaScript compiler will always look first in the local scope for a variable name, and if unsuccessful will move up into any/all enclosing functions/scopes looking for the same variable.
For more on scope-related behaviors, check out this article on hoisting in JavaScript.
Want to improve your JavaScript? I have a list of recommended JavaScript books.