With closures, you can add advanced features, such as implementing private variables, by reducing the amount and complexity of code. Closures are traditionally one of the features of purely functional programming languages. Closures are also a mainstream development language. Closures can greatly simplify complex operations and are often used in javascript libraries.
Understand the closure
Closures allow functions to access and manipulate variables outside the function. Closures enable functions to access variables or functions as long as they exist in the scope in which the function was declared.
The function declared by ⚠️ can be called at any time after the declaration, or even after the declared scope of the function has disappeared.
An 🌰
var outerValue = "ninja";
function outerFunction() {
assert(outerValue === "ninja","I can see the ninja.");
}
outerFunction();
function assert(value, text){
value&&console.log(text)
}
// I can see the ninja.
Copy the code
The outerFunction function in the above code can access the outerValue variable. Maybe we’ve written a lot of code like this without realizing that we’re actually creating a closure.
The outer variable outerValue and function outerFunction are both declared in a global scope, which (closures) never disappears (as long as the program is running). This function can access external variables because it is still scoped and visible.
Closures exist, but do not take advantage of closures. Change the code
🌰
var outerValue = "samurai";
var later;
function outerFunction() {
var innerValue = "ninja";
function innerFunction() {
assert(outerValue === "samurai", "I can see the samurai.");
assert(innerValue === "ninja", "I can see the ninja.");
}
later = innerFunction;
}
outerFunction();
later();
function assert(value, text){
value&&console.log(text)
}
// I can see the samurai.
// I can see the ninja.
Copy the code
The first assertion is easy to understand because outerValue is in global scope. The second assertion, however, is that after the outerFunction function executes, a reference to the innerFunction is assigned to the global variable later, which is then used to call the innerFunction.
When the innerFunction is executed, the scope of the outer function no longer exists, and there is an illusion that the innerValue is undefined. But when the function executes, the second assertion passes
When you declare an inner function in an outer function, you not only define the declaration of the function, but also create a closure. This closure contains not only the declaration of the function, but also all variables in that scope at the time the function was declared. When the inner function is finally executed, the original scope is still accessible through the closure, even though the declared scope is gone.
That’s the closure. Closures create bubbles of variables and functions in scope at the time they are defined, so the function gets what it needs when it executes.
Although these structures are not easy to see (there is no closure object that contains this much information to observe), storing and referencing this information directly affects performance. It is important to remember that every function that accesses a variable through a closure has a scope chain that contains all the information about the closure. So while closures are very useful, they should not be overused. With closures, all information is stored in memory and cleaned up until the JavaScript engine ensures that it is no longer used (it can be safely garbage collected) or the page is written
Using closures
Encapsulating private variables
JavaScript does not support private variables, but by using closures, we can achieve close, acceptable private variables
An 🌰
function Ninja() { var feints = 0; this.getFeints = function(){ return feints; }; this.feint = function(){ feints++; }; } var ninja1 = new Ninja(); ninja1.feint(); assert(ninja1.feints === undefined, "And the private data is inaccessible to us."); assert(ninja1.getFeints() === 1, "We're able to access the internal feint count."); var ninja2 = new Ninja(); Assert (ninja2.getFeints() === 0, "The second Ninja object gets it's own feints variable."); function assert(value, text){ value&&console.log(text) } // And the private data is inaccessible to us. // We're able to access the internal // The second Ninja object gets it's own feints variable.Copy the code
When you use the keyword new on a function, a new object instance is created, and the constructor is called with the new object as its context. So, this inside the function will point to the new instantiated object.
Inside the constructor, the variable feints is used to hold state, which can only be accessed inside the constructor due to JavaScript’s scoping rules. To make the variable accessible to code outside the scope, a method getFeints is defined to access the variable. There’s also the incremental method feint.
When the code executes, the object instance is visible via the variable Ninja. So the feint method is inside the closure and therefore has access to the variable feints. Outside the closure, not accessible
Through closures, internal variables feints can be maintained through instance objects without direct access. Because of scope rules, variables inside a closure can be accessed by methods inside the closure, and variables inside the closure cannot be accessed by code outside the constructor
The callback function
Dealing with callback functions is another common closure usage scenario, where we often need to access external data frequently
An 🌰
// <div id="box1">First Box</div> // <div id="box2">First Box</div> // var elem,tick, timer function animateIt(elementId) { var elem = document.getElementById(elementId); var tick = 0; var timer = setInterval(function(){ if (tick < 100) { elem.style.left = elem.style.top = tick + "px"; tick++; } else { clearInterval(timer); assert(tick === 100, "Tick accessed via a closure."); assert(elem, "Element also accessed via a closure."); assert(timer, "Timer reference also obtained via a closure." ); }}, 10); } animateIt("box1"); animateIt("box2"); // Tick accessed via a closure. // Element also accessed via a closure. // Timer reference also obtained via a closure. // Tick accessed via a closure. // Element also accessed via a closure. // Timer reference also obtained via a closure.Copy the code
The above code uses a separate anonymous function to animate the target element.
With closures, this anonymous function controls animation effects with three variables: ELEm, tick, and timer. These three variables are used to control the entire animation process and must be accessed in the global scope.
If we move these variables out of the animateIt function into the global scope, there will be some problems where the states of multiple different animations will conflict with three variables at the same time. For the animation to still work, you need to set three variables for each animation.
By defining variables inside the function, and based on closures, each animation gets its own private variables.
The above example writes clean and intuitive code with closures. By placing variables inside the animateIt function, we can create a default closure without requiring very complicated syntax.
Functions inside a closure can not only access these variables at the time of creation, but also update their values when functions inside the closure execute. A closure is not a snapshot of the state at the moment it is created, but rather a true encapsulation of the state, and variables can be modified as long as the closure exists.
Each animation gets private variables in the handler’s closure, and cannot access variables in other closures.
Closures are strongly scope-dependent
Trace code through execution context
In JavaScript, the basic unit of code execution is functions, which we use all the time.
As mentioned earlier, there are two types of JavaScript code: global code, defined outside all functions; One is the function code, which is inside the function. When the JavaScript engine executes code, each statement is in a specific execution context.
Since there are two types of code, there are two execution contexts: global execution context and function execution context. The most important difference is that there is only one global execution context, whereas a new function execution context is created each time the code is called.
An 🌰
function skulk(ninja) {
report(ninja + " skulking");
}
function report(message) {
console.log(message);
}
skulk("Kuma");
skulk("Yoshi");
// Kuma skulking
// Yoshi skulking
Copy the code
Explore how the execution context is created with this simple piece of code
- Each JavaScript program creates a global context and executes from the global execution context
- Define two functions skulk, report, and call skulk(“Kuma”) in the global code; . Because only certain code can be executed at any given time, the JavaScript engine stops executing the global code and starts executing the skulk function with the “Kuma” argument. Create a new function execution context
- The skulk function in turn calls the report function. Again, because only certain code can be executed at any one time, we pause the skulk function execution context and create a new report function execution context
- Report function is executed, and after completion, the code returns to skulk function, report function context is removed from the execution stack, skulk function execution context is reactivated, skulk function continues to execute
- Skulk is also removed from the stack after execution, reactivating the global execution context in wait.
- When skulk(“Yoshi”) is executed, it is similar to the above process, except that the parameters are changed
Use lexical context to track the scope of variables
Lexical environments are used internally by JavaScript engines to track the mapping between representations and specific variables.
In general, the lexical environment is associated with a particular JavaScript code structure, which can be a function, a snippet, or a try-catch statement
The lexical environment is primarily based on code nesting, and each time code is executed, the code structure gets a lexical environment associated with it. For example, each time a function is called, a new function lexical environment is created
In addition to tracking local variables, function declarations, function parameters, and the lexical environment, it is also necessary to track the external (parent) lexical environment. Because we need to access variables in the external code structure, if we cannot find an identifier in the current environment, the external environment is looked up. The lookup is stopped when a matching variable is found, or when an error is returned because the corresponding identifier is still not found in the global environment.
Whenever you create a function, you create a lexical environment associated with it. Whenever a function is called, a new execution environment is created.
Understand JavaScript variable types
The variability of variables
In JavaScript, variables can be defined by three keywords: var, let, and const. These three keywords differ in two ways: variability, and relation to lexical context
A variable defined by const is immutable and can only be set once when it is declared
The values declared by let and var can be changed any number of times
Define the keyword and lexical environment for variables
They are classified (by scope) by their relationship to the lexical environment. Var is a class and let is a class with const
The var keyword
When the keyword var is used, the variable is defined inside the nearest function or in the global lexical environment
An 🌰
var globalNinja = "Yoshi"; function reportActivity(){ var functionActivity = "jumping"; for(var i = 1; i < 3; i++) { var forMessage = globalNinja + " " + functionActivity; assert(forMessage === "Yoshi jumping","Yoshi is jumping within the for block"); assert(i, "Current loop counter:" + i); } assert(i === 3 && forMessage === "Yoshi jumping", "Loop variables accessible outside of the loop"); } reportActivity(); assert(typeof functionActivity === "undefined" && typeof i === "undefined" && typeof forMessage === "undefined", "We cannot see function variables outside of a function"); function assert(value, text){ value&&console.log(text) } // Yoshi is jumping within the for block // Current loop counter:1 // Yoshi is jumping within the for block // Current loop counter:2 // Loop variables accessible outside of the loop // We cannot see function variables outside of a functionCopy the code
In the above code, you define the global variable globalNinja first, and then the reportActivity function, which loops globalNinja through the function. As you can see, the recircular body has normal access to the variables forMessage and I in the block-level scope, functionActivity in the function body, and globalNinja
Note ⚠️ : variables defined in the block-level scope can still be accessed outside the block-level scope.
This comes from the fact that variables declared by var are actually always registered in the nearest function or in the global lexical environment without concern for block-level scope,
So in the example above, the forMessage variable is actually registered in the reportActivity function (the nearest function environment), even though it is contained in the block-level scope inside the for loop.
There are three lexical environments in the above code :(inside the nearest function or in the global lexical environment)
- The globalNinja variable is defined in the global environment
- Function environment created by the reportActivity function, including functionActivity, I, forMessage
- The block-level scope of the for loop, which is ignored when variables are defined through the var keyword
Define a variable with block-level scope using let and const
Unlike var, which defines variables in the nearest function or global lexical context, let and const are more straightforward. Let and const define variables directly in the nearest lexical context (block-level scope, inside a loop, inside a function, inside a global context)
Modify the previous 🌰
const globalNinja = "Yoshi"; function reportActivity(){ const functionActivity = "jumping"; for(let i = 1; i < 3; i++) { let forMessage = globalNinja + " " + functionActivity; assert(forMessage === "Yoshi jumping", "Yoshi is jumping within the for block"); assert(i, "Current loop counter:" + i); } console.log(typeof i,typeof forMessage); assert(typeof i === "undefined" && typeof forMessage === "undefined", "Loop variables not accessible outside the loop"); } reportActivity(); assert(typeof functionActivity === "undefined" && typeof i === "undefined" && typeof forMessage === "undefined", "We cannot see function variables outside of a function"); function assert(value, text){ value&&console.log(text) } // Yoshi is jumping within the for block // Current loop counter:1 // Yoshi is jumping within the for block // Current loop counter:2 // undefined undefined // Loop variables not accessible outside the loop // We cannot see function variables outside of a functionCopy the code
When a variable is declared with let and const, the variable is defined in the nearest environment. In the above code, the variables forMessage and I are defined in the block-level scope, functionActivity is defined in reportActivity, and globalNinja is defined in the global environment.
How closures work
Closures have access to all variables in the scope in which the function was created. Closures are closely related to scope.
Review closure emulation of private variables 🌰
function Ninja() { var feints = 0; this.getFeints = function(){ return feints; }; this.feint = function(){ feints++; }; } var ninja1 = new Ninja(); assert(ninja1.feints === undefined, "And the private data is inaccessible to us."); ninja1.feint(); assert(ninja1.getFeints() === 1, "We're able to access the internal feint count."); var ninja2 = new Ninja(); Assert (ninja2.getFeints() === 0, "The second Ninja object gets it's own feints variable."); function assert(value, text){ value&&console.log(text) } // And the private data is inaccessible to us. // We're able to access the internal // The second Ninja object gets it's own feints variable.Copy the code
Analyze the code above and use the identifier principle to understand how closures work in this case. Each time a constructor is called with the keyword new, a new lexical environment is created that holds local variables inside the constructor.
- After the new keyword is used, a new object is instantiated
- After entering the constructor, a new lexical environment is created that keeps track of all local variables created in the scope, and in the above code, it always keeps a reference to the feints variable
- During constructor execution, it creates two functions and assigns their values to the newly created object (getFeints, feint). Like any other environment, both functions retain a reference to the environment in which they were created
Whenever an object is created, a reference to the lexical environment is kept. Each instance of an object created through a constructor gets its own method, and each instance method contains its own variables when the constructor is called
Warnings about private variables
JavaScript never prevents us from copying properties created in one object to another
An 🌰
function Ninja() { var feints = 0; this.getFeints = function(){ return feints; }; this.feint = function(){ feints++; }; } var ninja1 = new Ninja(); ninja1.feint(); var imposter = {}; imposter.getFeints = ninja1.getFeints; assert(imposter.getFeints () === 1, "The imposter has access to the feints variable!" ); function assert(value, text){ value&&console.log(text) } // The imposter has access to the feints variable!Copy the code
The ninja1 method getFeints is assigned to a new imposter object, and the ImPoster object’s getFeints method is used to access the Ninja constructor variable feints.
So there are no real private object properties in JavaScript, but you can implement an acceptable private variable scheme through closures
conclusion
Closures provide access to all of the variables in the environment in which the closure was created. Closures create a “safety bubble” for functions and variables in the scope in which the function was created. This way, the function gets everything it needs to execute even if the scope in which the function was created disappears
We can use some of the advanced features of closures
- The private properties of an object are simulated by the variables and constructors inside the constructor
- Handle callback functions to simplify code
The JavaScript engine tracks the execution of the function by executing the context stack. Each time a function is called, a new function execution context is created and pushed to the top of the call stack. When the function completes, the execution context for the function is pushed from the call stack
JavaScript engine tracks identifiers through lexical context (scope)
The keyword var defines the nearest function-level variable or global variable
The let keyword and const define variables at the nearest level, including block-level variables
Closures are a side effect of JavaScript’s lease domain rules. Functions can still be called when the rented domain in which they were created disappears