Closures
Aclosure is the combination of a function bundled together (enclosed) with references to its surrounding state (thelexical environment). In other words, a closure gives a function access to its outer scope. In JavaScript, closures are created every time a function is created, at function creation time.
Lexical scoping
Consider the following example code:
function init() { var name = "Mozilla"; // name is a local variable created by init function displayName() { // displayName() is the inner function, that forms a closure console.log(name); // use variable declared in the parent function } displayName();}init();
init()
creates a local variable calledname
and a function calleddisplayName()
. ThedisplayName()
function is an inner function that is defined insideinit()
and is available only within the body of theinit()
function. Note that thedisplayName()
function has no local variables of its own. However, since inner functions have access to the variables of outer scopes,displayName()
can access the variablename
declared in the parent function,init()
.
If you run this code in your console, you can see that theconsole.log()
statement within thedisplayName()
function successfully displays the value of thename
variable, which is declared in its parent function. This is an example oflexical scoping, which describes how a parser resolves variable names when functions are nested. The wordlexical 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.
Scoping with let and const
Traditionally (before ES6), JavaScript variables only had two kinds of scopes:function scope andglobal scope. Variables declared withvar
are either function-scoped or global-scoped, depending on whether they are declared within a function or outside a function. This can be tricky, because blocks with curly braces do not create scopes:
if (Math.random() > 0.5) { var x = 1;} else { var x = 2;}console.log(x);
For people from other languages (e.g., C, Java) where blocks create scopes, the above code should throw an error on theconsole.log
line, because we are outside the scope ofx
in either block. However, because blocks don't create scopes forvar
, thevar
statements here actually create a global variable. There is alsoa practical example introduced below that illustrates how this can cause actual bugs when combined with closures.
In ES6, JavaScript introduced thelet
andconst
declarations, which, among other things liketemporal dead zones, allow you to create block-scoped variables.
if (Math.random() > 0.5) { const x = 1;} else { const x = 2;}console.log(x); // ReferenceError: x is not defined
In essence, blocks are finally treated as scopes in ES6, but only if you declare variables withlet
orconst
. In addition, ES6 introducedmodules, which introduced another kind of scope. Closures are able to capture variables in all these scopes, which we will introduce later.
Closure
Consider the following code example:
function makeFunc() { const name = "Mozilla"; function displayName() { console.log(name); } return displayName;}const myFunc = makeFunc();myFunc();
Running this code has exactly the same effect as the previous example of theinit()
function above. What's different (and interesting) is that thedisplayName()
inner function is returned from the outer functionbefore being executed.
At first glance, it might seem unintuitive that this code still works. In some programming languages, the local variables within a function exist for just the duration of that function's execution. OncemakeFunc()
finishes executing, you might expect that thename
variable would no longer be accessible. However, because the code still works as expected, this is obviously not the case in JavaScript.
The reason is that functions in JavaScript form closures. Aclosure is the combination of a function and the lexical environment within which that function was declared. This environment consists of any variables that were in-scope at the time the closure was created. In this case,myFunc
is a reference to the instance of the functiondisplayName
that is created whenmakeFunc
is run. The instance ofdisplayName
maintains a reference to its lexical environment, within which the variablename
exists. For this reason, whenmyFunc
is invoked, the variablename
remains available for use, and "Mozilla" is passed toconsole.log
.
Here's a slightly more interesting example—amakeAdder
function:
function makeAdder(x) { return function (y) { return x + y; };}const add5 = makeAdder(5);const add10 = makeAdder(10);console.log(add5(2)); // 7console.log(add10(2)); // 12
In this example, we have defined a functionmakeAdder(x)
, that takes a single argumentx
, and returns a new function. The function it returns takes a single argumenty
, and returns the sum ofx
andy
.
In essence,makeAdder
is a function factory. It creates functions that can add a specific value to their argument. In the above example, the function factory creates two new functions—one that adds five to its argument, and one that adds 10.
add5
andadd10
both form closures. They share the same function body definition, but store different lexical environments. Inadd5
's lexical environment,x
is 5, while in the lexical environment foradd10
,x
is 10.
Practical closures
Closures are useful because they let you associate data (the lexical environment) with a function that operates on that data. This has obvious parallels to object-oriented programming, where objects allow you to associate data (the object's properties) with one or more methods.
Consequently, you can use a closure anywhere that you might normally use an object with only a single method.
Situations where you might want to do this are particularly common on the web. Much of the code written in front-end JavaScript is event-based. You define some behavior, and then attach it to an event that is triggered by the user (such as a click or a keypress). The code is attached as a callback (a single function that is executed in response to the event).
For instance, suppose we want to add buttons to a page to adjust the text size. One way of doing this is to specify the font-size of thebody
element (in pixels), and then set the size of the other elements on the page (such as headers) using the relativeem
unit:
body { font-family: Helvetica, Arial, sans-serif; font-size: 12px;}h1 { font-size: 1.5em;}h2 { font-size: 1.2em;}
Such interactive text size buttons can change thefont-size
property of thebody
element, and the adjustments are picked up by other elements on the page thanks to the relative units.
Here's the #"size-12").onclick = size12;document.getElementById("size-14").onclick = size14;document.getElementById("size-16").onclick = size16;
<button>12</button><button>14</button><button>16</button><p>This is some text that will change size when you click the buttons above.</p>