Closure seems to be perceived as some kind of mysterious concept and is probably one of the most dreaded of all JS interview questions. For some part of the dev community at least.
I am part of that part of the community. Since explaining sharpens understanding, then by way of an explanation I expect to clear some of that closure mystery.
Closure is a form of space division or partition. That is the way I conceive of it. Since partition of space comes with rules of inclusion and exclusion, knowing those rules will help grasp closure. Scope is the first thing to examine.
Scope
Scope can be looked at through two closely related angles. Scope as space and scope as rules.
Scope as space
MDN opens its definition of scope with the idea of space, in the form of context :
“The current context of execution. The context in which values and expressions are "visible" or can be referenced.”
Scope is an area where variables are visible, that is accessible to a function. As such, scope is a spatial relation between what can see and what can be seen. In other words, scope is a function’s visual field and that visual field is governed by rules.
Scope as rules
In the Scope and Closures volume of his You Don’t Know JS series, Kyle Sympson defines scope as the set of rules that governs the retrieval of variables in a computer program (Scope and Closures, 2014, p.11). Those rules guide both the compiler that produces executable code, and the programmer who writes the source code.
Declaring a function that references a variable stored in a place where the compiler hasn’t been told to search means program failure. It is up to the programmer to follow the protocol.
If the programmer cannot change the protocol, it is the way he decides to write his code that determines the units of scope. That prerogative of the author of code is called lexical scoping. When lexical scoping applies, scope is set relatively to where a function is declared. That’s the programmer’s choice. It is not the only way that scope is set and some languages use dynamic scoping, which sets scope based on where variables are declared. That is to say, when the compiler looks for a variable’s value, it searches for what has been assigned to it most recently.
let x = 2;
function foo(a) { return x + a }
function bar() { let x = 3; return foo(0) }
bar();
With lexical scoping, bar()
would evaluate to 2. If JS had dynamic scoping, it would evaluate to 3.
The rules of scope restrict the size the function’s visual field (or search area if we look at it through compiler eye). What is the visual field of a function made up of ? A function has access to its outer scope (including outer of outer etc) and its own inner scope, but not to other functions’ inner scopes (for example a sibling or child function, ie. the inner scope of functions that are either contained in outer scope or inner scope).
Going back to the previous program, foo()
has access to x = 2
, which is found in the outer scope. It does not however have access to x = 3
, which is found in its sibling’s scope.
When looking for a variable the compiler always starts looking in the inner scope of the function. If search fails there, the compiler will look in the outer scope, if it fails there, it will go to the outer scope of the outer scope and all the way up to the global scope, if needed. If nothing is found there, search stops since the global scope does not have an outer scope.
With dynamic scoping, when foo()
is executed and the compiler needs to get the value assigned to a variable name, it will look for the most recent value assigned to that variable, which is 3.
Now is a good time to bring back the idea of context. The word “context” is a synonym of “surrounding”, and the idea of surrounding is at the heart of closure.
Closure
Closure aliases
Closure has a few aliases, like Closed Over Variable Environment (C.O.V.E.), Persistent Lexical Scope Referenced Data (P.L.S.R.D.) or the “backpack” to name a few (Will Sentance coined the last one, and his workshops on Frontend Masters are incredibly useful and accessible).
Even though they refer to the same thing, all three aliases focus on a different angle of the concept. C.O.V.E. emphasizes the enclosing process at play in a closure, P.L.S.R.D. focuses on the persistence of data and “backpack” underlines the idea that stuff is being carried around.
That which is carried around is a variable environment or in other words, a piece of lexical scope. How does this happen ?
Closure as a bundle/backpack
As was said earlier the rules of scope mean that a function has access to variables in the outer scope and its own inner scope, as long as those scopes do not belong to other functions’ inner scopes. Closure is the thing that makes it possible for a function that is executing outside of its original lexical environment to access all the variables of that environment (Scope and Closures, 2014, p. 48). Making it seem as though some inner scope is accessed from outer scope. For MDN a closure can be conceived as the
“combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment)”
Moreover, to close over a variable environment and make it persistent, a function has to be returned.
Some examples
ReferenceError
function tellSecret() { return secret }
function hideSecret() {
let secret = “I ate all the cake”;
tellSecret(secret)
}
hideSecret(); // ReferenceError: secret is not defined
This is how you would expect things to work. Invoking hideSecret()
throws a ReferenceError, since tellSecret()
, which is called from the inner scope, references a variable secret
as parameter that is nowhere to be found in its outer or inner scope. Sure that variable is sitting right next to it in hideSecret
’s inner scope, but tellSecret
does not have access to its sibling’s inner scope.
Truth comes out
function hideSecret() {
let secret = “I ate all the cake”;
return function needToSay() {
return secret;
}
}
let tellSecret = hideSecret();
tellSecret(); // “I ate all the cake”
When a function is executed, it is pushed onto the call stack and a new execution context is created. Within that execution context, variables are accessible following the rules of scope. When execution reaches a return statement or the bottom of the function, it is popped off the stack and the execution context is erased. The variable environment enclosed in the function’s inner scope vanishes. However, with closure, that variable environment persists. That is what happens above.
The return value of hideSecret()
is assigned to a variable called tellSecret
. That return value is needToSay
’s function declaration. When slapping a pair of parentheses at the end of tellSecret
, it is the code inside of needToSay
that is being executed, bundled together with its lexical environment. The value of secret
is being returned, which is nowhere to be found in global scope. Even if hideSecret
has been popped off the call stack, by returning needToSay
, a record has been made of that lexical environment, and that is closure.
One thing and the other
function tellSecret(cb) {
let secret = " I did NOT eat the cake";
return cb(secret);
}
function hideSecret() {
let secret = "I ate all the cake";
function sayOneThing(a) {
return function sayAnother(b) {
return a + " " + b;
}
}
return tellSecret(sayOneThing(secret));
}
let s = hideSecret();
s(); // "I ate all the cake I did NOT eat the cake"
First tellSecret
is declared, then hideSecret
and then the return value of hideSecret
is assigned to the variable s
. What does hideSecret
return ? It returns a call to tellSecret
, with the function sayOneThing
passed as parameter. So hideSecret
should return whatever tellSecret
returns. What does the call to tellSecret
evaluate to ? The return value of tellSecret
will be whatever the function that is passed as parameter returns. So tellSecret
should return whatever sayOneThing
returns. What does sayOneThing
return ? The return value of sayOneThing
is the definition of a function called sayAnother
. So invoking s
amounts to calling sayAnother
, and sayAnother
returns the concatenation of whatever parameter was passed in sayOneThing
(“I ate all the cake”) and sayAnother
(“I did NOT eat the cake”). It is because sayAnother
is bundled with a record of sayOneThing
’s lexical environment that it can return a variable from an execution context that looks as though it’s gone. That is what closure is.
I think.
Top comments (0)