Translation of an article by Richard Cornford
Javascript Closures .
- Introduction
- Resolving object property names
- Assignment of values
- Reading values
- Identifier Name Resolution, Execution Contexts, and Scoping Chain
- Execution context
- The scope chain and the [[scope]] property
- Identity Name Resolution
- ...
Introduction
Short circuit
A closure is an expression (usually a function) that can have free variables, along with an environment that binds these variables (that is, “closes” this expression).
Closures are among the most powerful features of ECMAScript (javascript), but they cannot be properly applied without understanding. Despite the fact that they are easy to create, even by accident, their creation can have detrimental effects, in particular, in some relatively common browser environments. To avoid accidental collisions with flaws and to take advantage of closures, it is necessary to understand their mechanism. This is highly dependent on the role of the scope chain in identifier resolution resolution and on the resolution of property names in objects.
The simplest explanation of the closure is that ECMAScript allows nested functions, function definitions and function expressions (function expressions) within the bodies of other functions. And these nested functions have access to all local variables, parameters and functions that are inside their external function (external functions). A closure occurs when one of these nested functions becomes available outside the function in which it was included, so it can be executed after the external function completes. At this point, she still has access to local variables, parameters, and internal function declarations (function declarations) of her external function. These local variables, parameters and function declarations (initially) have the same values ​​that were at the time of the completion of the external function and can interact with the internal function.
')
Unfortunately, a proper understanding of closures requires an understanding of the mechanisms behind them, and quite a few technical details. Although some of the algorithms defined in ECMA 262 are affected at the beginning of the following explanation, most cannot be omitted or simply reduced to a simplified form. If you are familiar with resolving object property names, you can skip this section, but only people who are already familiar with closures can afford to skip subsequent sections and stop reading right now and return to using them.
Resolving object property names
ECMAScript recognizes two categories of objects: native objects “Native Object” and objects of the environment “Host Object” and a subcategory of native objects called built-in objects “Built-in Objects” (ECMA 262 3rd Ed Section 4.3). Native objects belong to the language, and host objects are provided by the environment and can be, for example, a document object, DOM nodes, etc.
Native objects are free and dynamic containers of named properties (some implementations are not so dynamic when it comes to the subcategory of embedded objects, although this usually does not matter). Certain named properties of a native object store values ​​that can be referenced by another object (functions are also objects in this sense) or elementary values: String, Number, Boolean, Null, or Undefined. The primitive type Undefined is a bit unusual in the sense that you can set the property of an object to
undefined , but the property will not be removed from the object; it will remain a named property of the object, which simply stores the value
undefined .
The following is a simplified explanation of how the properties of objects are read and set, with as much internal details as possible, including internal details.
Assignment of values
Named properties of objects can be created, or the values ​​of existing named properties can be set, by assigning a value to a named property.
In this way,
var objectRef = new Object();
A property named "testNumber" can be created like this:
objectRef.testNumber = 5; objectRef["testNumber"] = 5;
The object did not have the
testNumber property before assigning a value, but it was created after. Any subsequent assignments do not need to create this property, they will simply change its value.
objectRef.testNumber = 8; objectRef["testNumber"] = 8;
Javascript objects have prototypes that can themselves be objects, as will be briefly described later, and these prototypes can have named properties. But this does not apply to assignment. If a value is assigned and the object does not have a property with the corresponding name, then this property will be created and the value assigned to it. If the object has such a property, its value will be reset.
Reading values
It is when reading values ​​that prototype objects are used. If the object has a property with the name used in the property accessor expression (property accessor), then the value of this property will be returned.
objectRef.testNumber = 8; var val = objectRef.testNumber;
But all objects can have prototypes, and prototypes are objects and, in turn, can have prototypes that can have prototypes, etc., forming what is called a chain of prototypes. A prototype chain ends when one of the objects in the chain has a
null prototype. The default prototype with the
Object constructor has a
null prototype, so
var objectRef = new Object();
creates an object with the prototype
Object.prototype , which itself has a
null prototype. Then the prototype chain of the
objectRef object contains only one object:
Object.prototype . But,
function MyObject1(formalParameter){ this.testNumber = formalParameter; } function MyObject2(formalParameter){ this.testString = formalParameter; } MyObject2.prototype = new MyObject1( 8 ); var objectRef = new MyObject2( "String_Value" );
The instance of the
MyObject2 object referenced by the
objectRef variable has a prototype chain. The first object in this chain is an instance of the object
MyObject1 , which was created and assigned to the
prototype property of the constructor
MyObject2 . An instance of
MyObject1 has as its prototype an object that has been assigned to the
prototype property of the
MyObject1 function by default. This prototype is the default prototype of an
Object , i.e. the object referenced by the
Object.prototype .
Object.prototype has a
null prototype, so the chain ends at this point.
When a property access expression tries to read a named property from an object referenced by an
objectRef , the whole prototype chain can participate in the process. In the simple case
var val = objectRef.testString;
- an instance of
MyObject2 , accessible via
objectRef , has a property named
testString , therefore the value of this property is set to
String_Value , and it is assigned to the variable
val .
However,
var val = objectRef.testNumber;
cannot read a property from the very instance of the
MyObject2 function, since it does not have this property, but the variable
val is set to
8 , not
undefined , because the interpreter checks the object, which is its prototype, due to an unsuccessful search for the corresponding named property in the object itself. Its prototype is an instance of the function
MyObject1 , which was created with the
testNumber property with a value of
8 assigned to this property, so the property access expression is calculated as the value
8 . Neither
MyObject1 nor
MyObject2 defines the
toString method, but if the property access expression tries to read the value of the
toString property from the
objectRef ,
var val = objectRef.toString;
then the variable
val is assigned a function reference. This function is the
toString property of the
Object.prototype object and is returned as a result of the prototype verification process: the
objectRef object is
checked , after the absence of the
toString property is
detected , the
objectRef prototype is
checked and when it turns out that it does not have this property, in turn its prototype is checked. Its prototype is the
Object.prototype , which has a
toString method and the link is returned to this function.
Finally:
var val = objectRef.madeUpProperty;
- returns
undefined , because the process that processes the prototype chain does not find properties with the name
madeUpProperty in any of the objects, it ultimately reaches the prototype of the
Object.prototype object, i.e.
null , and then the process ends, returning
undefined .
Reading named properties returns the first value found from an object or from its prototype chain. Assigning a value to a named property of an object will create a property in the object itself if the corresponding property does not already exist.
This means that if the value was assigned to
objectRef.testNumber = 3 , then the
testNumber property will be created in the instance of the
MyObject2 function and subsequent attempts to read the value will result in the return of the value that is set in the object. Now, the execution of the prototype chain processing is no longer required to execute the property access expression, but the instance of the
MyObject1 object with the value
8 assigned to the
testNumber property is not changed. Assigning a value to an
objectRef simply hides the corresponding property in its prototype chain.
Note that ECMAScript defines an internal
[[prototype]] internal property of type
Object . This property is not directly accessible to scripts, but there is a chain of objects referenced by the
[[prototype]] internal property, which is used in resolving access property expressions; this chain is a chain of object prototypes. The public property
prototype exists for assigning values, defining, and manipulating prototypes along with the internal
[[prototype]] property. Details of the relationship between these two properties are described in ECMA 262 (3rd edition) and are not included in this discussion.
Identifier Name Resolution, Execution Contexts, and Scoping Chain
Execution context
The execution context is an abstract concept that is used in the ECMAScript specification (ECMA 262 3rd edition) to define the behavior required from ECMAScript implementations. The specification says nothing about how the
execution context should be implemented, but
execution contexts have associative attributes that refer to structures defined in the specification, so they can be conceived (or even implemented) as objects with properties, albeit closed ones.
All javascript code is executed in
execution context . The global code (embedded code in the html page or in the JS file or executed after the page loads (loads)) is executed in the
global execution context and each function call (possibly as a constructor) has a corresponding execution context. The code executed using the
eval function also receives a specific execution context, but since
eval is usually not used by javascript programmers, it will not be discussed here. Specified details of
execution contexts can be found in section 10.2 of ECMA 262 (3rd edition).
When the javascript function is called, it adds
the execution context , if another function is called (or the same function recursively), a new
execution context is created, and the execution process adds this context during the function call. When the called function is completed, the initial
execution context is returned. Thus, the javascript executable code forms a stack of
execution contexts .
When the
execution context is created, several things happen in a certain order. First, in
the execution context , an Activation object is created. The activation object is another mechanism from the specification. It can be considered as an object, because as a result it has available named properties, but it is not an ordinary object, since it does not have a prototype (at least, the prototype is not defined) and there can be no links to it in javascript code.
The next step in creating
the execution context for a function call is to create a
arguments object, this is a massive object with elements indexed by integers that correspond to the arguments sent to the function in the given order. It also has the
length and
callee properties (which are not relevant to our discussion, details in the specification). A property of the activation object is created with the name
arguments , which is assigned a reference to the
arguments object.
Then
the execution context assigns a
scope .
The scope consists of a list (or chain) of objects. Each function object has an internal
[[scope]] property (which we will soon discuss in more detail), which also consists of a list (or chain) of objects.
The scope assigned to
the execution context of a function call consists of the list referenced by the
[[scope]] property of the corresponding function object, and of the activation object added to the beginning of the chain (or at the top of this list).
Then comes the process of creating variables (variable instantiation) using an object, which is defined in ECMA 262 as an object of variables (Variable object). At the same time, the activation object is used as an object of variables (note that this is one and the same object, this is important). Named properties of the variable object are created for each formal parameter of the function, and if the arguments in the function call correspond to these parameters, then the arguments are assigned to these properties (otherwise, they are assigned
undefined ). The definition of internal functions is used to create function objects that are assigned to the properties of the variable object with the names corresponding to the function names used in the function declarations. The final stage in creating variables is to create named object properties for variables that correspond to all local variables defined inside the function.
Properties created in the object of variables that correspond to declared local variables initially get the value
undefined during the creation of variables; local variables are not initialized until the corresponding assignment operation is performed during execution of the function body code.
In fact, an activation object with the
arguments property and an object of variables with named properties corresponding to the local variables of the function are the same object that allows you to treat the
arguments identifier as a local variable of the function.
Finally, the value used with the this keyword is assigned. If this value refers to an object, then property access expressions with the prefix of
this refer to the properties of this object. If this value (assigned inside the function) is
null , then
this will refer to the global object.
The processing of the global execution context is slightly different, it has no arguments, so it does not need to define an activation object to refer to them. The global execution context needs a scope and its chain contains only one object - a global object. The global execution context goes through the creation of variables, its internal functions are the usual top-level declaration functions that make up most of the javascript code. A global object is used as an object of variables, which is why functions declared globally become properties of a global object. The same with globally declared variables.
The global execution context also uses the reference to the global object as the value of
this .
The scope chain and the [[scope]] property
The scope chain of the execution context of a function call is assembled by adding an activation object (variable object) to the beginning of
the scope chain contained in the
[[scope]] property of the function object, so it is important to understand how the
[[scope]] internal property is defined.
In ECMAScript, functions are objects, they are created during the creation of variables from function declarations, during the execution of a function-expression (Function Expression) or by calling the
Function constructor.
Function objects created by the
Function constructor always have the
[[scope]] property referencing the
scope chain that contains only the global object.
Function objects created by a function declaration or function-expression have
the scope of the execution context in which they are created, which is assigned to their internal
[[scope]] property.
In the simplest case, the declaration of a global function, such as
function exampleFunction(formalParameter){ ...
the corresponding function object is created during the creation of variables for the global execution context. The global execution context has a
scope consisting of only a global object.
, ,
exampleFunction ,
[[scope]] ,
, .
-
var exampleFuncRef = function(){ ...
, , , , . ,
[[scope]] .
- , ,
. , ,
function exampleOuterFunction(formalParameter){ function exampleInnerFuncitonDec(){ ...
, , ,
[[scope]] , .
exampleOuterFunction , ( ).
, , ,
[[scope]] ( ). , ,
[[scope]] , .
, .
.
[[scope]] [[scope]] ( ). ECMAScript
with .
with ,
( /).
with ( )
.
with , , -
with var y = {x:5};
exampleFuncWith ,
, .
with ,
y ,
, -.
[[scope]] , -, ,
, .
y , , .
with ,
(
y ),
[[scope]] y .
. ECMA 262
this , , , ..
this , , ,
.
. , , . - ,
, ( ).
, . , ( ) , , .
, , . ,
, , . .
( ) , , , , , . ( ).
.
Javascript [ 2]