Closures are a basic concept in JavaScript that every JavaScript developer should know and understand. However, many new JavaScript developers are confused by this concept.
Understanding closures correctly can help you write better, more efficient, and cleaner code. At the same time, this will help you become a better JavaScript developer.
So, in this article, I’ll try to parse the internals of closures and how they work in JavaScript.
So, without further ado, let’s get started.
What is a closure
In short, a closure is a function that has access to the scope of its outer function, even if that outer function has returned. This means that even after the function has executed, the closure can remember and access the variables and parameters of its external function.
Before we dive into closures, let’s first understand lexical scope.
What is lexical scope
Lexical scope (or static scope) in JavaScript refers to the accessibility of variables, functions, and objects within the physical location of the source code. Here’s an example:
let a = 'global';
function outer() {
let b = 'outer';
function inner() {
let c = 'inner'
console.log(c); // prints 'inner'
console.log(b); // prints 'outer'
console.log(a); // prints 'global'
}
console.log(a); // prints 'global'
console.log(b); // prints 'outer'
inner();
}
outer();
console.log(a); // prints 'global'
Copy the code
The inner function has access to variables defined in its own scope and to the scope of the outer function as well as to the global scope. The outer function, on the other hand, can access variables defined in its own scope as well as in the global scope. So, a scope chain of the above code looks like this:
Global {
outer {
inner
}
}
Copy the code
Notice that the inner function is bounded by the lexical scope of the outer function, which in turn is bounded by the global scope. This is why inner functions can access outer functions as well as variables defined in the global scope.
Real-world examples of closures
Before diving into how closures work, let’s take a look at some practical examples of closures.
1 / / examplesfunction person() {
let name = 'Peter';
return function displayName() {
console.log(name);
};
}
let peter = person();
peter(); // prints 'Peter'
Copy the code
In this code, we call the person function, which returns the internal function displayName, and store that function in the perter variable. When we call the perter function (actually referring to the displayName function), the name “perter” is printed to the console. But no variable named name is defined in the displayName function, so even if the function returns, the function can somehow access the variable of its external function, person. So the displayName function is actually a closure.
2 / / examplesfunction getCounter() {
let counter = 0;
return function() {
returncounter++; }}letcount = getCounter(); console.log(count()); // 0 console.log(count()); // 1 console.log(count()); / / 2Copy the code
Similarly, we return an anonymous inner function by calling the getCounter function and storing it in the count variable. Since the count function is now a closure, you can access the variable Couneter of the getCounter function even after the getCounter function has returned. Note, however, that the value of counter does not reset to 0 each time the count function is called, as it usually does. This is because each time count() is called, a new function scope is created, but only one scope is created for the getCounter function. Since the variable counter is defined within the scope of the getCounter function, the value of the count function is incrementing each time it is called instead of resetting to 0.
How closures work
So far, we’ve discussed what closures are and some practical examples. Let’s see how closures work in javaScript. To truly understand how closures work in JavaScript, we must first understand two important concepts in JavaScript: 1) execution context and 2) lexical environment.
Execution Context
An execution context is an abstract environment in which JavaScript code is evaluated, evaluated, and executed. When global code executes, it executes in the global execution context, and function code executes in the function execution context.
Currently there can only be one running execution environment (because JavaScript is a single-threaded language), which is managed by a stack data structure called an execution stack or a call stack.
The execution stack is a stack with a LIFO (last in, first out) structure, where options can only be added or removed at the top of the stack.
The execution context that is currently running is always at the top of the stack, and when the executing function completes its execution, its execution context is removed from the stack and control reaches the execution context below it on the stack.
Let’s look at a code snippet to better understand the execution context and stack.
When the above code executes, the JavaScript engine creates a global execution context to execute the global code, and then when the first() function is called, it creates a new execution context for that function and pushes it to the top of the execution stack. So, the execution stack of the above code looks like this:
When the first() function finishes executing, its execution stack is removed from the stack. Control then moves to the next execution context, the global execution context. Therefore, the remaining code in the global scope will be executed.
Lexical environment
Each time the JavaScript engine creates an execution context to execute a function or global code, it also creates a new lexical environment to store variables defined in that function during its execution.
A lexical environment is a data structure that contains an identifier-variable map. (The identifier here refers to the name of a variable or function, while a variable is a reference to the actual object (including objects of function type) or the original value).
A lexical environment has two components :(1) environmental data and (2) references to the external environment.
1. Environment data refers to where variables and function declarations are actually stored.
2. A reference to an external environment means that it has access to an external (parent) lexical environment. This component is important and is key to understanding how closures work.
A lexical environment looks like this conceptually:
lexicalEnvironment = { environmentRecord: { <identifier> : <value>, <identifier> : <value> } outer: < Reference to the parent lexical environment> //Copy the code
Now let’s take a look at the previous code snippet:
let a = 'Hello World! ';
function first() {
let b = 25;
console.log('Inside first function');
}
first();
console.log('Inside global execution context');
Copy the code
When the JavaScript engine creates a global execution context to execute global code, it also creates a new lexical environment to store variables and functions defined in the global scope. Therefore, the lexical context of the global scope would look like this:
globalLexicalEnvironment = {
environmentRecord: {
a : 'Hello World! ',
first : < reference to function object >
}
outer: null
}
Copy the code
The external lexical environment is set to NULL here because the global scope has no external lexical environment. When the engine creates an execution context for the first () function, it also creates a lexical environment to store variables defined in that function during its execution. So the lexical context of the function looks like this:
functionLexicalEnvironment = {
environmentRecord: {
b : 25,
}
outer: <globalLexicalEnvironment>
}
Copy the code
The external lexical environment for a function is set to the global lexical environment because the function is surrounded by the global scope in the source code.
A detailed closure example
Now that we understand the execution context and lexical context, let’s return to closures.
Example a
Let’s take a look at this code block
function person() {
let name = 'Peter';
return function displayName() {
console.log(name);
};
}
let peter = person();
peter(); // prints 'Peter'
Copy the code
When the Person function executes, the JavaScript engine creates a new execution context and lexical environment for the function. When the function completes, the displayName function is returned and assigned to the perter variable. So its lexical environment looks like this:
personLexicalEnvironment = {
environmentRecord: {
name : 'Peter',
displayName: < displayName function reference>
}
outer: <globalLexicalEnvironment>
}
Copy the code
When the Person function completes execution, its execution context is removed from the stack. But its lexical environment is still in memory because its lexical environment is referenced by the lexical environment of its internal displayName function. So the variable is still available in memory.
When the Peter function executes (which is actually a reference to the displayName function), the JavaScript engine creates a new execution context and lexical environment for the function. So its lexical environment looks like this:
displayNameLexicalEnvironment = {
environmentRecord: {
}
outer: <personLexicalEnvironment>
}
Copy the code
Because the displayName function does not declare variables, its environment data is empty. During the execution of this function, the javaScript engine will attempt to find the variable name in the function’s lexical environment. Since the lexical environment of the displayName function doesn’t have any variables, the engine looks for the outer lexical environment, which is the lexical environment of the Person function that is still in memory. The JavaScript engine finds the variable name and prints it to the console.
Example 2
function getCounter() {
let counter = 0;
return function() {
returncounter++; }}letcount = getCounter(); console.log(count()); // 0 console.log(count()); // 1 console.log(count()); / / 2Copy the code
Similarly, the lexical context for the getCounter function looks like this:
getCounterLexicalEnvironment = {
environmentRecord: {
counter: 0,
<anonymous function> : < reference to function>
}
outer: <globalLexicalEnvironment>
}
Copy the code
This function returns an anonymous function and assigns it to the variable count. When the count function executes, its lexical context looks like this:
countLexicalEnvironment = {
environmentRecord: {
}
outer: <getCountLexicalEnvironment>
}
Copy the code
When the count function is called, the Javascript engine tries to find the variable counter in the function’s lexical environment. Again, because its environment data is empty, the engine will look in the lexical environment around the function. So, after the first call to the count function, the lexical context for the getCounter function looks like this:
getCounterLexicalEnvironment = {
environmentRecord: {
counter: 1,
<anonymous function> : < reference to function>
}
outer: <globalLexicalEnvironment>
}
Copy the code
On each call to the count function, the Javascript engine creates a new lexical environment for the count function, increments the count variable, and updates the lexical environment for the getCounter function to indicate that the change has been made.
conclusion
So we learned what closures are and how they work. Closures are a basic JavaScript concept that every JavaScript developer should understand. Familiarity with these concepts will help you become a more efficient and better JavaScript developer. If you find this article helpful, please give it a like! (after)
Afterword.
The above translation is only used for learning communication, the level is limited, there are inevitably mistakes, please correct me.
The original
The original link