Naveera A.
—How do JavaScript closures work? And why are they used?
Closures are one of the trickier parts of JavaScript. But once you’re past your “Aha!” moment, you will appreciate their beauty.
To understand how closures work, you first need to understand the concept of scope, scope chain, and lexical scope.
You can think of scope as the boundaries inside which a variable is accessible. Consider the following example:
function printName() { const name = 'nav'; console.log(name); } printName(); // logs 'nav'
The variable name
belongs to the scope created inside the function printName()
. You can access it inside the function printName()
. But if you try to access name
from outside printName()
, JavaScript will throw a reference error:
function printName() { const name = 'nav'; } printName(); console.log(name); //ReferenceError: name is not defined
In JavaScript, every code block (if
blocks, for
loops) and function has its own scope. You can think of the curly braces '{}'
as gates. These gates prevent the variables from leaking out.
If you declare a variable outside any block of code or function, it will be globally scoped. A variable in the global scope can be accessed everywhere in the code.
In the following example, the variable name
is global, which means it is accessible everywhere:
const name = 'nav'; function printName() { console.log(name); } printName(); // logs 'nav' console.log(name); // logs 'nav'
Scopes can be nested inside each other, just like a function can be nested inside another function.
Consider the following example:
function outerFunction() { const name = 'nav'; function innerFunction() { console.log(name.toUpperCase()); } innerFunction(); } outerFunction(); // logs 'NAV'
The variable name
is inside the scope of outerFunction()
, but innerFunction()
can access it. Keeping the gates analogy in mind, curly braces '{}'
prevent the inner variables from going out, but they allow the outer variables to come in.
The scope chain is a stack of all accessible scopes, from the most immediate context to the global context.
The scope chain of innerFunction()
consists of its local scope, scope of outerFunction()
, and the global scope (the outermost scope).
The scope chain of outerFunction()
consists of its local scope and the global scope.
Now consider the following example:
const name = 'nav'; function outerFunction() { const name = 'jane'; function logName() { console.log(name); } logName(); } outerFunction(); // logs 'jane'. Overwriting the variable inside the // local scope only. console.log(name); // logs 'nav'. The global variable didn't change
The variable name
is a global variable. When it is redeclared inside the scope of outerFunction()
, a new local variable is created with the same name and a new value. Inside the scope of outerFunction()
, the local variable name
is used.
Every time a variable is initialized, the interpreter will first look it up in its own scope. If the variable is not found in the local scope, then the interpreter will look it up in the parent scope. If it is not found in the parent scope, the interpreter will look it up in the grandparent scope, and so on, until it reaches the global scope.
The variables of the parent scope (outer scope) are accessible inside the children scope (inside scope), but not the other way round.
What happens if you define the function logName()
outside the outerFunction()
, and call it from within outerFunction()
?
let name = 'nav'; function logName() { console.log(name); } function outerFunction() { let name = 'jane'; logName(); } outerFunction(); // logs 'nav'
You may think that logName()
will log 'jane'
, as it was called within the scope of outerFunction()
. But logName()
logs the value nav
.
This is lexical scoping. Lexical scope means that the scope chain (“What is the parent scope for this variable?”) is determined by where functions are defined in the code base, not where functions are executed.
According to MDN documentation:
This is an example of lexical scoping, which describes how a parser resolves variable names when functions are nested. The word lexical refers to the fact that lexical scoping uses the location where a variable is declared within the source code to determine where that variable is available. Nested functions have access to variables declared in their outer scope.
In the example above, the scope chain of logName()
, at the time of its definition, contains the global variable name = 'nav'
and not the local variable name = 'jane'
.
In the last example we declared the logName()
function in a global scope, but called it from within outerFunction()
.
What will happen if we do it the other way round? For example:
function outerFunction() { const name = 'nav'; function logName() { console.log(name); } return logName; } const myFunc = outerFunction(); myFunc(); // logs 'nav'
The function logName()
is declared inside the scope of outerFunction()
. JavaScript has first-class functions, which means it is possible for outerFunction()
to return a reference to logName()
when outerFunction()
is executed.
The logName()
function is now disguised as myFunc
, and you can call it from outside the scope of outerFunction()
.
The logName()
function has its scope chain, which includes the variable name
. The interesting thing is that it remembers name
, or closes over it, even after outerFunction()
has finished its execution. The variable name
wasn’t cleaned up or destroyed at the end of execution of outerFunction()
. This is how it logs the value of name
.
The logName()
function forms a closure. A closure is a function that is bundled together with references to its surrounding state.
In other words, a closure is a combination of an inner function and its lexical scope, which includes the parent or outer scope. It can access its lexical scope even if it is executed outside of it.
Closures are useful in many situations. For example, when you need to create private variables or define a behavior that is attached to an event. The latter is used extensively in event-based frontend JavaScript.
Tasty treats for web developers brought to you by Sentry. Get tips and tricks from Wes Bos and Scott Tolinski.
SEE EPISODESConsidered “not bad” by 4 million developers and more than 100,000 organizations worldwide, Sentry provides code-level observability to many of the world’s best-known companies like Disney, Peloton, Cloudflare, Eventbrite, Slack, Supercell, and Rockstar Games. Each month we process billions of exceptions from the most popular products on the internet.
Here’s a quick look at how Sentry handles your personal information (PII).
×We collect PII about people browsing our website, users of the Sentry service, prospective customers, and people who otherwise interact with us.
What if my PII is included in data sent to Sentry by a Sentry customer (e.g., someone using Sentry to monitor their app)? In this case you have to contact the Sentry customer (e.g., the maker of the app). We do not control the data that is sent to us through the Sentry service for the purposes of application monitoring.
Am I included?We may disclose your PII to the following type of recipients:
You may have the following rights related to your PII:
If you have any questions or concerns about your privacy at Sentry, please email us at compliance@sentry.io.
If you are a California resident, see our Supplemental notice.