JavaScript: closure (Closures)
Introduction
In this chapter, we will introduce closure, a common topic in JavaScript ). In fact, we have all talked about closures. However, we should try to discuss the closure from a theoretical point of view to see how the closure in ECMAScript actually works.
As mentioned in the previous article, these articles are a series of articles that are related to each other. Therefore, to better understand the content described in this article, we recommend that you read chapter 1 Scope chain and Chapter 14th variable objects first.
Overview
Before directly discussing the ECMAScript closure, it is necessary to take a look at some basic definitions in functional programming.
As we all know, in Functional Languages (ECMAScript also supports this style), functions are data. For example, a function can be assigned a value to a variable. When a parameter is passed to another function, it can also be returned from the function. Such functions have special names and structures.
Definition
A functional argument ("Funarg")-is an argument which value is a function. function-type parameter ("Funarg") -- refers to A parameter whose value is a function.
Example:
function exampleFunc(funArg) { funArg();}exampleFunc(function () { alert('funArg');});
In the above example, the actual parameter funarg is actually an anonymous function passed to exampleFunc.
In turn, a function that accepts a function-type parameter is called a high-order function (HOF ). It can also be called a function or partial mathematical or operator. In the above example, exampleFunc is such a function.
Previously mentioned, a function can be used not only as a parameter but also as a return value. These functions return values are called functions with function values (functions with functional value or function valued functions ).
(function functionValued() { return function () { alert('returned function is called'); };})()();
Functions that can exist in the form of normal data (for example, when a parameter is passed, a function-type parameter is accepted or returned with a function value) are called first-class functions (generally the first-class objects ). In ECMAScript, all functions are the first class objects.
A function can exist as a normal data (for example, when a parameter is passed, a function-type parameter is accepted or returned as a function value ).
In ECMAScript, all functions are the first class objects.
A function that accepts itself as a parameter is called an auto-applicative function or self-applicative function ):
(function selfApplicative(funArg) { if (funArg && funArg === selfApplicative) { alert('self-applicative'); return; } selfApplicative(selfApplicative);})();
Auto-replicative function or self-replicative function ). Generally, the word "self-replication" is used in literature:
(function selfReplicative() { return selfReplicative;})();
One of the more interesting modes of the auto-copy function is to accept only one item of the set as a parameter, instead of accepting the set itself.
// Receives the function registerModes (modes) {modes. forEach (registerMode, modes);} // use registerModes (['roster', 'accounts', 'groups']); // function modes (mode) declared by the self-replication function) {registerMode (mode); // register a mode return modes; // return the function itself} // usage, modes chained call modes ('roster') ('accounts ') ('groupup') // a bit similar: jQueryObject. addClass (). toggle (). removClass (B)
However, directly transferring a set is relatively effective and intuitive.
Variables defined in functional parameters can be accessed when "funarg" is activated (because the variable object storing context data is created every time it enters the context ):
Function testFn (funArg) {// when funarg is activated, the local variable localVar can access funArg (10); // 20 funArg (20 ); // 30} testFn (function (arg) {var localVar = 10; alert (arg + localVar );});
However, we know from Chapter 14th that in ECMAScript, a function can be encapsulated in a parent function and can use variables in the context of the parent function. This feature causes funarg problems.
Funarg Problems
In a stack-oriented programming language, Partial Variables of a function are stored on the stack. Each time a function is activated, these variables and function parameters are pushed onto the stack.
When the function returns, these parameters are removed from the stack. This model imposes great restrictions on using a function as a function type value (for example, returning a function as a return value from the parent function ). In most cases, the problem occurs when the function has a free variable.
A free variable is a variable used in a function. It is neither a function parameter nor a local variable of the function.
Example:
function testFn() { var localVar = 10; function innerFn(innerParam) { alert(innerParam + localVar); } return innerFn;}var someFn = testFn();someFn(20); // 30
In the above example, localVar is a free variable for the innerFn function.
For systems that use the stack-oriented model to store local variables, this means that when the testFn function call ends, its local variables will be removed from the stack. In this way, when the innerFn function is called from outside, an error occurs (because the localVar variable does not exist ).
Moreover, in the stack-oriented implementation model, it is impossible to return innerFn with a returned value. Because it is also a local variable of the testFn function, it will be removed with the return of testFn.
Another problem is when the system uses dynamic scopes and functions as function parameters.
Take the following example (pseudo code ):
Var z = 10; function foo () {alert (z);} foo (); // 10-When static and dynamic scopes are used (function () {var z = 20; foo (); // 10-use static scope, 20-use dynamic scope })(); // when using foo as a parameter, it is the same (function (funArg) {var z = 30; funArg (); // 10-static scope, 30-dynamic scope }) (foo );
We can see that the system using dynamic scopes and variables (identifiers) are managed through the dynamic stack of variables. Therefore, free variables are queried in the current active dynamic chain, rather than in the static scope chain saved during function creation.
In this way, a conflict occurs. For example, even if Z still exists (opposite to the previous example of removing a variable from the stack), there is still a problem: in different function calls, which of the following is the value of Z (from which context and scope )?
The above describes two types of funarg problems-depending on whether the function is returned as a return value (the first type of problem) and whether the function is used as a function parameter (the second type of problem ).
To solve the above problem, the concept of closure is introduced.
Closure
Closure is the combination of a code block and Context data that creates the code block.
Let's take a look at the following example (pseudo code ):
Var x = 20; function foo () {alert (x); // free variable x = 20} // For the foo closure fooClosure = {call: foo // reference to function lexicalEnvironment: {x: 20} // context of the Search context };
In the preceding example, "fooClosure" is a pseudo-code. Correspondingly, in ECMAScript, the "foo" function already has an internal Attribute-create the scope chain of the context of the function.
"Lexical" is usually omitted. In the preceding example, Context data is saved when the closure is created. When the function is called next time, the free variable can be found in the saved (closure) context. As shown in the code above, the value of the variable "z" is always 10.
In the definition, we use the generalized word "code block". However, we usually use functions that we often use (in ECMAScript. Of course, not all implementations of the closure will bind the closure with the function. For example, in Ruby, the closure may be a procedure object ), A lambda expression or code block.
Stack-based implementation is obviously not applicable to the implementation of saving local variables after the context is destroyed (because it is in conflict with the stack-based structure ). Therefore, in this case, the closure data in the upper-level scope is implemented by dynamically allocating memory (based on the implementation of "heap"), and the garbage collector (GC) is used together) and reference counting ). This implementation method has lower performance than stack-based implementation. However, any implementation can always be optimized: You can analyze whether a function uses free variables, function parameters, or function values, then decide whether to store data in the stack or heap based on the situation.
Implementation of ECMAScript Closure
After discussing the theory, let's introduce how closure is implemented in ECMAScript. It is necessary to emphasize that ECMAScript only uses static (lexical) scopes (while languages such as Perl can use static scopes or dynamic scopes for variable Declaration ).
Var x = 10; function foo () {alert (x) ;}( function (funArg) {var x = 20; // variable x in (lexical) static storage in the context. funArg (); // 10 instead of 20}) (foo) is saved when the function is created );
Technically, the data in the parent context of the function is stored in the internal attribute [Scope] of the function. If you do not know what [Scope] is, we recommend that you read chapter 1, which describes [Scope] in detail. If you fully understand the [[Scope] and Scope chain knowledge, then you fully understand the closure.
According to the algorithm created by the function, we can see that in ECMAScript, all functions are closures, because they all save the scope chain of the Upper-layer context when they are created (except for exceptions) (whether or not the function will be activated later -- [[Scope] will be available when the function is created ):
Var x = 10; function foo () {alert (x);} // foo is the closure foo:
= {[Call]:
, [[Scope]: [global: {x: 10}],... // other attributes };
As we said, for the purpose of optimization, when a function does not use free variables, the implementation may not be saved in the sub-scope chain. However, nothing in the ECMA-262-3 specification. Therefore, normally, all parameters are stored in the [[Scope] attribute during creation.
In some implementations, direct access to the closure scope is allowed. For example, for the [Scope] attribute of a function, Rhino has a non-standard _ parent _ attribute, which is described in Chapter 12th:
Var global = this; var x = 10; var foo = (function () {var y = 20; return function () {alert (y );};})(); foo (); // 20 alert (foo. _ parent __. y); // 20foo. _ parent __. y = 30; foo (); // 30 // you can move to the top alert (foo. _ parent __. _ parent _ = global); // truealert (foo. _ parent __. _ parent __. x); // 10
All objects reference A [Scope]
Note that in ECMAScript, the closures created in the same parent context share a [Scope] attribute. That is to say, if a closure modifies the variable [[Scope], it will affect the reading of the variable by other closures:
This means that all internal functions share the same parent scope.
Var firstClosure; var secondClosure; function foo () {var x = 1; firstClosure = function () {return ++ x ;}; secondClosure = function () {return -- x ;}; x = 2; // affects AO [x]. alert (firstClosure () in two [Scope] public closed packets ()); // 3, through the [[Scope]} foo (); alert (firstClosure (); // 4 alert (secondClosure (); // 3
There is a common misunderstanding about this function. Developers often fail to get the expected results when creating a function (counting internally) in a loop statement, it is expected that each function has its own value.
Var data = []; for (var k = 0; k <3; k ++) {data [k] = function () {alert (k );};} data [0] (); // 3, instead of 0 data [1] (); // 3, instead of 1 data [2] (); // 3, instead of 2
The above example proves that the closure created in the same context shares a [[Scope] attribute. Therefore, the variable "k" in the upper context can be easily changed.
ActiveContext. scope = [... // other variable objects {data: [...], k: 3} // activity object]; data [0]. [[Scope] === Scope; data [1]. [[Scope] === Scope; data [2]. [[Scope] = Scope;
In this way, when the function is activated, the final k used is changed to 3. As shown in the following figure, creating a closure can solve this problem:
Var data = []; for (var k = 0; k <3; k ++) {data [k] = (function _ helper (x) {return function () {alert (x) ;};}) (k); // input the k value} // The result is correct. data [0] (); // 0 data [1] (); // 1 data [2] (); // 2
Let's see what happened to the above Code? After the function "_ helper" is created, it is activated by passing in the "k" parameter. The returned value is also a function, which is saved in the corresponding array element. This technique produces the following results: When a function is activated, a new variable object will be created each time "_ helper" contains the parameter "x ", the value of "x" is the value of "k" passed in. In this way, the [[Scope] of the returned function is as follows:
Data [0]. [[Scope] = [... // activity object AO: {data: [...] in the parent context of other variable objects, k: 3}, _ activity object AO: {x: 0}] In the helper context; data [1]. [[Scope] = [... // activity object AO: {data: [...] in the parent context of other variable objects, k: 3}, _ activity object AO: {x: 1}] In the helper context; data [2]. [[Scope] = [... // activity object AO: {data: [...] in the parent context of other variable objects, k: 3}, _ activity object AO: {x: 2}] In the helper context;
We can see that the [[Scope] attribute of the function has the desired value. To achieve this purpose, we have to create additional variable objects in [[Scope. Note that, in the returned function, if you want to obtain the value of "k", the value will still be 3.
By the way, many articles about JavaScript believe that only the newly created function is the closure, which is wrong. In practice, this method is the most effective. However, theoretically, all functions in ECMAScript are closures.
However, the method mentioned above is not the only method. You can obtain the correct "k" value in other ways, as shown below:
Var data = []; for (var k = 0; k <3; k ++) {(data [k] = function () {alert (arguments. callee. x );}). x = k; // use k as an attribute of the function} // The result is also correct data [0] (); // 0 data [1] (); // 1 data [2] (); // 2
Funarg and return
Another feature is returned from the closure. In ECMAScript, the Return Statement in the closure returns the control flow to the call context (caller ). In other languages, for example, Ruby, there are many closure forms, and the corresponding processing closure returns are also different. The following methods are possible: they may be directly returned to the caller, or in some cases -- exit directly from the context.
The exit behavior of ECMAScript is as follows:
Function getElement () {[1, 2, 3]. forEach (function (element) {if (element % 2 = 0) {// return to the forEach function // instead of the getElement function alert ('found: '+ element); // found: 2 return element ;}}); return null ;}
However, in ECMAScript, try catch can achieve the following effects:
Var $ break ={}; function getElement () {try {[1, 2, 3]. forEach (function (element) {if (element % 2 = 0) {// return alert ('found: '+ element) from getElement; // found: 2 $ break. data = element; throw $ break;});} catch (e) {if (e = $ break) {return $ break. data ;}} return null;} alert (getElement (); // 2
Theoretical version
It is explained that developers often mistakenly interpret the closure as returning internal functions from the parent context, or even as that only anonymous functions can be closures.
Besides, because of the scope chain, all functions are closures (not related to function types: anonymous functions, FE, NFE, and FD are closures ).
Only one type of Function exists, that is, the Function created through the Function constructor, because its [[Scope] only contains global objects.
To better clarify this issue, we provide two correct version definitions for the closure in ECMAScript:
In ECMAScript, the closure refers:
Theoretically: All functions. Because they all save the upper-layer context data when they are created. This is true even for simple global variables, because accessing global variables in a function is equivalent to accessing free variables. In this case, the outermost scope is used.
From a practical perspective: The following functions are regarded as closures:
Even if the context for creating it has been destroyed, it still exists (for example, the internal function returns from the parent function)
A free variable is referenced in the code.
Closure usage practice
In actual use, the closure can create a very elegant design that allows you to customize multiple computing methods defined on funarg. The following is an example of array sorting. It accepts a sorting condition function as a parameter:
[1, 2, 3]. sort (function (a, B) {... // sorting condition });
In the same example, the array map method maps the original array to a new array based on the conditions defined in the function:
[1, 2, 3].map(function (element) { return element * 2;}); // [2, 4, 6]
Using function parameters, you can easily implement a search method and support unlimited search conditions:
someCollection.find(function (element) { return element.someProperty == 'searchCondition';});
There are also application functions, such as the common forEach method, which applies the function to each array element:
[1, 2, 3].forEach(function (element) { if (element % 2 != 0) { alert(element); }}); // 1, 3
By the way, the apply and call methods of function objects can also be used as application functions in functional programming. The apply and call methods have already been introduced when we talk about "this". Here, we regard them as application functions -- functions applied to parameters (the parameter list is in apply, independent parameters in call ):
(function () { alert([].join.call(arguments, ';')); // 1;2;3}).apply(this, [1, 2, 3]);
The closure also has another very important application-delayed calling:
var a = 10;setTimeout(function () { alert(a); // 10, after one second}, 1000);
There are also callback Functions
//... Var x = 10; // only for examplexmlHttpRequestObject. onreadystatechange = function () {// called only when data is ready; // here, no matter in which context you create // The value of the variable "x" already exists alert (x); // 10 };//...
You can also create an encapsulated scope to hide the secondary object:
Var foo = {}; // initialization (function (object) {var x = 10; object. getX = function _ getX () {return x ;}) (foo); alert (foo. getX (); // get the closure x-10
Summary
This article introduces