In the smoky, dimly lit room of JavaScript, there's a concept lurking in the shadows, waiting to be unveiled. We call it a "closure." Now, hang on to your hats, because we're about to embark on a journey into the heart of this enigmatic creature.
When functions can be treated as values and local bindings are recreated upon each function call, an intriguing question arises: What occurs with these local bindings when the function call that created them is no longer active?
For example;
function rememberValue(n) {
let local = n;
return () => local;
}
let wrap1 = rememberValue(1);
let wrap2 = rememberValue(2);
console.log(wrap1());
// → 1
console.log(wrap2());
// → 2
This is permitted and works as you'd hope - both instances of the binding remain accessible. This scenario effectively illustrates that local bindings are created afresh for each function call, and different calls do not interfere with each other's local bindings.
What are local bindings?
First, what are bindings??
In JavaScript, the term 'binding' is the formal language used to describe what many individuals commonly call a variable
let totalHoursSpentOnArticle = 42;
const name = "Ford Arthur";
Bindings that are defined outside of any function or block have a scope that encompasses the entire program. As a result, you can reference such bindings from any part of your code. These bindings are commonly referred to as "global" bindings.
Bindings declared using let
and const
are, in fact, local to the block where they are declared. Therefore, if you create one of these bindings within a loop, the code segments located before and after the loop lack visibility of it. In the JavaScript version prior to 2015, scopes were only introduced by functions. Consequently, old-style bindings, created with the var
keyword, remained accessible throughout the entirety of the function in which they were defined, or at the global scope if they were not within a function
Bindings generated for function parameters or declared within a function have a scope limited to that specific function, earning them the name of local bindings. Upon each function call, fresh instances of these bindings are generated. This separation between functions offers a degree of isolation, where each function call operates within its own unique context (local environment), often requiring minimal awareness of the broader global environment.
let x = 10;
if (true) {
let y = 20;
var z = 30;
console.log(x + y + z);
// → 60
}
console.log(x + y)
// → Uncaught ReferenceError: y is not defined
// y is not visible here
console.log(x + z);
// → 40
The variables y
and z
are the local bindings in this code sample. x
is a global binding.
Every scope possesses the capability to "look into" the scope that surrounds it, making x
visible inside the block within the provided example. However, an exception arises when multiple bindings share the same name - under such circumstances, the code can exclusively access the innermost binding with that name. For instance, when the code within the square
function references n
, it specifically refers to its own n
and not the n
in the global scope.
const square = function(n) {
return n ** 2;
};
let n = 42;
console.log(square(2));
// → 4
console.log(n);
// → 42
This behavior, wherein a variable's scope is dictated by its position within the source code hierarchy, serves as an instance of lexical scoping. Lexical scoping enables inner functions, like square
in this case, to access variables from their enclosing scopes based on their respective positions within the code's structure.
Lexical scoping
Closures and lexical scope are frequently intertwined and misunderstood by many in the JavaScript community.
Lexical scope refers to how nested functions can access variables defined in their enclosing scopes. This behavior is illustrated in the code block above…
Now, let's direct our gaze to the central element: Closures.
What is a closure?
A closure is the combination of a function enveloped with references to its surrounding context (the lexical environment). To put it differently, closure provides the means to reach the scope of an outer function from within an inner function.
To make use of a closure, create a function within another function and make it accessible. You can render a function accessible by either returning it or passing it to another function.
Even after the outer function has been completed and returned, the inner function will retain access to the variables in the outer function's scope.
Note the first part of the definition; "A closure is the combination of a function enveloped with references to its surrounding context". That's essentially a description of lexical scope!
But we require the inclusion of the second part of this definition to give an example of a closure… "Even after the outer function has been completed and returned, the inner function will retain access to the variables in the outer function's scope."
Let us examine fascinating instances of closures;
function theAnswer() {
var say = () => console.log(answer);
// Local variable that ends up within the closure
var answer = "'Forty-two,' said Deep Thought";
return say;
}
var tellUs = theAnswer();
tellUs(); // ''Forty-two,' said Deep Thought'
Here, we have two functions;
An outer function
theAnswer
has a variableanswer
and returns the inner functionsay
.An inner function that returns the variable
answer
whentellUs
is invoked…
Duplicate the provided code example and give it a go
Let's attempt to understand and comprehend what's unfolding…
In the code example;
theAnswer
is called and creates an environment in which it defines a local variable answer and returns an inner function say.Next, we call the
theAnswer
function and save the resulting inner function in the variabletellUs
. Logging the variabletellUs
returns something like this;
ƒ theAnswer() {
var say = () => console.log(answer);
// Local variable that ends up within the closure
var answer = "'Forty-two,' said Deep Thought";
return say;
}
- So that when we later call
tellUs
, it proceeds to run the inner function say, which subsequently logs the value ofanswer
to the console.
'Forty-two,' said Deep Thought
Getting your head around programs like these requires a bit of mental gymnastics. Picture function values as these packages carry both their inner workings and the surroundings they grew up in. When you evoke them, they bring that homey environment they were born in - no matter where you make the call from.
Use cases of closures
Data Privacy
Closures can be used to create private variables and functions. This ensures that certain data is not accessible from outside the scope, providing a level of data privacy and encapsulation.
Here's an illustration:
function createCounter() {
let count = 0; // Private variable enclosed by the closure
return function() {
count++;
return count;
};
}
const incrementCounter = createCounter();
console.log(incrementCounter()); // Output: 1
console.log(incrementCounter()); // Output: 2
Allow me to offer a succinct breakdown of its operation:
createCounter
is a function that initializes by establishing a private variable named count with an initial value of 0 within its own scope.It returns an enclosed function, a closure, which, when called, increases the
count
by 1 and returns the current value.incrementCounter
is assigned the result of invokingcreateCounter
, which means it holds the inner function (closure) that was returned.When
incrementCounter()
is called, it triggers the execution of the inner function, resulting in the increase of thecount
and the return of the updated value.With every further use of
incrementCounter()
,count
increments.
Callback functions
Closures are often used in dealing with asynchronous tasks and event-driven programming. They help create callback functions that remember and work with their surrounding environment, which is crucial when explaining event-based systems in documentation.
Here's a code example demonstrating closures in callback functions:
function fetchData(url, callback) {
setTimeout(function() {
const data = `Data from ${url}`;
callback(data); // Closure captures 'data'
}, 1000); // Simulate a 1-second delay
}
function processData(data) {
console.log(`Processing: ${data}`);
}
fetchData('https://example.com/api/data', processData);
In our scenario, the
fetchData
function simulates an asynchronous data retrieval process. It accepts both a URL and a callback function as its parameters.Inside the
setTimeout
, it generates some data and invokes the callback function, which captures the data variable from its surrounding scope due to closure.The
processData
function serves as our callback, responsible for handling and logging the data that we receive.We call
fetchData
with a URL and theprocessData
callback, demonstrating how closures empower the callback to reach and employ thedata
variable from its surrounding scope.
Closures are a fundamental component of functional programming and may be found everywhere in JavaScript as well as in other languages (not only functional ones).
More use cases are;
You can definitely check out more here on wikipedia
Clos(ure)ing remarks, or thoughts? lmao 😂
So, there you have it - the tale of JavaScript closures. They may seem cryptic at first, but once you've grasped their essence, you'll find they're a powerful tool in your programming arsenal. Use them wisely, and you'll unlock new realms of possibility in your technical journey. Happy coding, my friends.
Okay real quick
What would this return??
for(var i = 0; i < 3; i++) {
const log = () => {
console.log(i);
}
setTimeout(log, 10);
}
More resources
MDN has a fantastic page on closures, as you might imagine, and the Wikipedia article on the topic goes into great length regarding further applications as well as how they function in other languages.
Top comments (4)
I don't know if the last question is a trick question or not but the condition is i > 3 due to which the loop will not run if it's a typo then the output will be 0,1,2
Indeed, the
i > 3
condition was a typo that has been fixed. However, it's worth noting that the code currently returns3,3,3
. This behavior stems from the usage of thevar
keyword within the for loop. I encourage you to replacevar
withlet
and observe the result.I just noticed that pesky 'var' keyword there. What's happening is, since we're using 'var,' it creates a global variable, and we're referencing that variable when logging. If we had used 'let,' its scope would have been limited to the iteration, preventing this behavior.
exactly !