Closures in Javascript [Part 1]

Translated by Richard Kornford Javascript Closures .

  • Introduction
  • Object Property Name Resolution
    • Value assignment
    • Reading values

  • Identifier name resolution, execution contexts, and scope chain
    • Execution context
    • The scope chain and the [[scope]] property
    • Identifier Name Resolution

  • ...

Introduction


Short circuit
A closure is an expression (usually a function) that can have free variables, along with the environment that binds these variables (ie, closes this expression).

Closures are among the most powerful features of ECMAScript (javascript), but they cannot be applied properly without understanding. Despite the fact that they are easy to create, even by accident, their creation can have detrimental consequences, in particular, in some relatively common browser environments. To avoid accidental collisions with faults and take advantage of faults, it is necessary to understand their mechanism. This greatly depends on the role of the scope chain in identifier resolution and the resolution of property names in objects.

The simplest explanation for closure is that ECMAScript allows nested functions, function definitions, and function expressions inside the bodies of other functions. And these nested functions have access to all local variables, parameters and functions inside their external function (external functions). A closure is formed when one of these nested functions becomes available outside the function in which it was included, so it can be executed after the completion of the external function. At this point, she still has access to the local variables, parameters, and internal function declarations of her external function. These local variables, parameters, and function declarations (initially) have the same meanings,

Unfortunately, a proper understanding of closures requires an understanding of the mechanisms behind them, and a lot of technical details. Although some of the algorithms defined in ECMA 262 are addressed 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 already familiar with closures can afford to skip subsequent sections and stop reading right now and return to using them.

Object Property Name Resolution


ECMAScript recognizes two categories of objects: Native Objects and Host Objects, and a subcategory of native objects called 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 a subcategory of built-in objects, although this usually does not matter). Certain named properties of a native object store values ​​that can be a reference to 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 deleted from the object; it will remain the 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, affecting the internal details as much as possible.

Value assignment


Named properties of objects can be created, or values ​​of existing named properties can be set by assigning a value to a named property.
Thus,
var objectRef = new Object(); // создает обобщенный (generic) объект javascript.

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 the value, but it was created after. Any subsequent assignments do not need to create this property, they will just change its value.
objectRef.testNumber = 8;
/* или  */
objectRef["testNumber"] = 8;

Javascript objects have prototypes, which themselves can be objects, as will be described briefly below, 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 a value assigned to it. If the object has such a property, then its value will be reset.

Reading values


It is when reading values ​​that prototypes of objects are used. If the object has a property with the name used in the property accessor expression, the value of this property will be returned
/* Присвоение значение именованному свойству. Если у объекта не было свойства
   с соответствующим именем до присваивания, то оно появится после
*/
objectRef.testNumber = 8;
/* Считываем значение из свойства */
var val = objectRef.testNumber;
/* и val теперь содержит значение 8, которое было только 
что присвоено именованному свойству объекта. */ 

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 prototype chain. The prototype chain ends when one of the objects in the chain has a null prototype . The prototype used by default with the Object constructor has a null prototype , so
var objectRef = new Object(); // создает обобщенный (generic) объект javascript

creates an object with a prototype Object.prototype , which itself has a null prototype . Then the prototype chain of the objectRef object contains only one object: Object.prototype . However,
/* Функция “конструктор” для создания объектов типа MyObject1.*/
function MyObject1(formalParameter){
    /*Возьмем свойство сконструированного объекта testNumber и
      присвоим ему значение, переданное конструктору, как его первый
      аргумент 
    */
    this.testNumber = formalParameter;
}
/* Функция “конструктор” для создания объектов типа MyObject2 */ 
function MyObject2(formalParameter){
   /*Возьмем свойство testString сконструированного объекта и 
     присвоим ему значение, переданное конструктору, как его первый 
     аргумент
    */
    this.testString = formalParameter;
}
/*Следующая операция заменит прототип, созданный по умолчанию, 
  ассоциированный со всеми экземплярами объекта MyObject2, 
  экземпляром объекта MyObject1, 
  отправив аргумент 8 в конструктор MyObject1, 
  тогда у его свойства testNumber будет установлено это значение
*/
MyObject2.prototype = new MyObject1( 8 );
/*Наконец, создадим экземпляр функции MyObject2 и присвоим переменной objectRef
  ссылку на этот объект, передав в конструктор строку как первый аргумент
*/
var objectRef = new MyObject2( "String_Value" );

The instance of MyObject2 referenced by the variable objectRef has a prototype chain. The first object in this chain is an instance of MyObject1 , which was created and assigned to the prototype property of the MyObject2 constructor . An instance of MyObject1 has the prototype object that was assigned to the prototype property of MyObject1 by default. This prototype is the default prototype of the Object , i.e. the object referenced by Object.prototype . Object.prototype has a null prototype, therefore, at this point the chain ends.

When a property access expression tries to read a named property from an object referenced by objectRef , then the entire prototype chain can participate in the process. In the simple case
var val = objectRef.testString;

- an instance of MyObject2 , accessible through objectRef , has a property called testString , so the value of this property is set to String_Value , and it is assigned to the variable val .
Nonetheless,
var val = objectRef.testNumber;

cannot read the property from the instance of the MyObject2 function , because it does not have such a property, but the variable val is set to 8 , and 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 MyObject1 function , which was created with the testNumber property with a value of 8 assigned to this property, so the property access expression is evaluated as 8 . Neither MyObject1 nor MyObject2 define a methodtoString , but if the property access expression tries to read the value of the toString property from objectRef ,
var val = objectRef.toString;

then the val variable will be assigned a function reference. This function is the toString property of the Object.prototype object and it is returned as a result of the prototype verification process: the objectRef object is checked , after detecting the absence of the toString property , the objectRef prototype is checked , and when it turns out that it does not have this property, in turn its prototype is being tested. Its prototype is Object.prototype , which has a toString method and the link returns to this function.

Finally:
var val = objectRef.madeUpProperty;

- returns undefined , because the process processing 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 the 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 a value was assigned to objectRef.testNumber = 3 , then the testNumber propertywill be created in the instance of the MyObject2 function and subsequent attempts to read the value will return the value that is set in the object. Now, the prototype chain is no longer required to execute the access expression for the property, but the instance of the MyObject1 object with the value 8 assigned to the testNumber property has not been changed. Assigning a value to an objectRef object simply hides the corresponding property in its prototype chain.

Note that ECMAScript defines the internal property [[prototype]] of the internal type Object. This property is not directly accessible to scripts, but there is a chain of objects referenced by the internal [[prototype]] property , which is used to resolve access property expressions; this chain is a prototype chain of an object. The public prototype property exists for assigning values, defining, and manipulating prototypes in conjunction with the internal [[prototype]] property . Details of the relationship between these two properties of honey are described in ECMA 262 (3rd edition) and are not included in this discussion.

Identifier name resolution, execution contexts, and scope chain


Execution context


The execution context is an abstract concept that is used in the ECMAScript specification (ECMA 262 3rd edition) to determine the behavior required by ECMAScript implementations. The specification does not say anything about how the execution context should be implemented, but execution contexts have associative attributes that refer to structures defined in the specification, therefore they can be conceived (or even implemented) as objects with properties, albeit closed.

All javascript code is executed in the execution context . Global code (embedded code in an html page or in a JS file or executed after the page loads (loads)) is executed in the global execution contextand each function call (possibly as a constructor) has a corresponding execution context. Code executed using the eval function also gets a specific execution context, but since eval is not commonly 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 a 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 terminates, the original is returned.execution context . Thus, the javascript code executing forms a stack of execution contexts .

When an execution context is created , several things happen in a specific order. First, in the context of execution , 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 accessible named properties, but it is not an ordinary object, because it doesn’t have a prototype (at least the prototype is not defined) and in javascript code there can be no links to it.

The next step in creating a execution context for calling a function is to create an arguments object, it is an array-like object with elements indexed by integers that match the arguments sent to the function in that order. It also has properties length and callee (which are not relevant to our discussion, details are 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 scope . A scope consists of a list (or chain) of objects. Each function object has an internal property [[scope]](which we will shortly examine in more detail), which also consists of a list (or chain) of objects. The scope assigned to the execution context of the function call consists of a list referenced by the [[scope]] property of the corresponding function object and an activation object added to the top of the chain (or to the top of this list).

Then the process of creating variables (variable instantiation) occurs using an object that 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 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 when calling the function correspond to these parameters, then the arguments are assigned to these properties (otherwise undefined) The definition of internal functions is used to create function objects assigned to the object properties of variables with names corresponding to the names of functions used in function declarations. The last step in creating variables is to create named properties of the variable object that correspond to all local variables defined inside the function.

The properties created in the variable object that correspond to the declared local variables initially get the value undefined during the creation of the variables; the local variables are not initialized until the corresponding assignment operation is executed during the execution of the function body code.

In fact, an activation object with the arguments propertyand an object of variables with named properties corresponding to local variables of the function is the same object that allows you to consider the identifier arguments as a local variable of the function.

And finally assigned value used to the keyword this . If this value refers to an object, then property access expressions prefixed with 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. 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 ordinary top-level declaration functions, which 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. Same thing with variables declared globally.

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 the function call is assembled by adding an activation object (variable object) to the top 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 an expression function (Function Expression), or when the Function constructor is called .

Function objects created by the Function constructor always have the [[scope]] property , which refers to the scope chainwhich 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 of a global function declaration, 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 only of a global object.

Thus, this created function object, referenced by the property of the global object exampleFunction , is assigned the internal property [[scope]] , which refers to the scope chain containing only the global object.

A similar scope chain will be assigned during execution of an expression function in a global context.
var exampleFuncRef = function(){
    ...   // код тела функции
}

except that in this case the named property of the global object is created during the creation of the variables of the global execution context, but the function object will not be created and the reference to it will not be assigned to the named property of the global object until the assignment expression is executed. But the creation of the function still occurs in the global execution context, therefore the [[scope]] property of the created function object contains only the global object in the assigned scope chain.

Internal declarations of functions and expression functions lead to the creation of function objects that are created within the context of function execution, so they get a more complex chain of scope. Consider the code below, which defines a function with a declaration of an internal function, and then executes an external function
function exampleOuterFunction(formalParameter){
    function exampleInnerFuncitonDec(){
        ... // тело внутренней функции
    }
    ...  // остальная часть тела внешней функции
}
exampleOuterFunction( 5 );

A function object corresponding to the declaration of an external function is created during the creation of variables in the global execution context, therefore its [[scope]] property contains a scope chain consisting of one global object.

When the global code makes an exampleOuterFunction call , a new execution context is created for this function call along with the activation object (variable object). The scope of the new execution context becomes a chain consisting of a new activation object, followed by the chain referenced by the [[scope]] propertyexternal function (just a global object). The process of creating variables for the new execution context will lead to the creation of a function object corresponding to the declaration of the internal function, the [[scope]] property of this internal function will be assigned the value of the scope of the execution context in which it was created. The scope chain contains an activation object, followed by a global object.

Up to this point, everything is automatically executed and controlled by the structure and execution of the source code. The execution scope scope chain defines the [[scope]] properties for the created function objects and these properties of the function objects [[scope]]determine the scope for their execution context (together with the corresponding activation object). But ECMAScript supports the with statement as a means of changing scope.

The with statement evaluates the expression, and if it is an object, it is added to the scope of the current execution context (right before the activation object / variables). Then with evaluates another expression (which may be a block) and then restores the scope chain of the execution context to its original form. With

statementcannot affect the declaration of the function, since the creation of function objects occurs during the creation of variables, but expression functions can be executed inside the with statement
/* создадим глобальную переменную y, которая ссылается на объект */
var y = {x:5}; // объектный литерал со свойством x
function exampleFuncWith(){
    var z;
    /* Поставим объект, на который ссылается переменная y,
        в начало цепи областей видимости
    */
    with(y){
        /* вычислим функцию-выражение, чтобы создать объект функции,
           и присвоим ссылку на этот объект функции локальной
           переменной z
        */
        z = function(){
            ... // тело внутренней функции-выражения;
        }
    }
    ... 
}
/* выполним функцию exampleFuncWith */
exampleFuncWith();

When exampleFuncWith is called , its execution context has a scope chain consisting of the activation object of this function and the following global object. Executing the with statement adds the object referenced by the global variable y to the top of this scope chain for the duration of this expression function. The property of the [[scope]] function object created by the execution of the expression function is assigned a value that corresponds to the scope of the execution context in which this object was created. Scope consists of object yfollowed by the activation object from the context of the execution of the external function, followed by the global object.

When the block expression of the with statement completes, the scope of the execution context is restored (the y object is deleted), but the function object was created at that moment and its [[scope]] property was assigned a link to the scope chain with the y object at its top.

Identifier Name Resolution


Identifiers are searched through a chain of scopes . ECMA 262 defines this as a keyword rather than an identifier, which is not unreasonable, as it is determined depending on the value of this , which is in the context of the execution in which it is used, without resorting to the chain of scope .

Identifier name resolution begins with the first object in the scope chain . The process finds out if there is a property with a name that matches the identifier. Due to the fact that the scope chainis a chain of objects, then this test covers the chain of prototypes of this object (if any). If the corresponding value cannot be found in the first object in the scope chain , then the search continues in the next object. And so on until a property with a name corresponding to the identifier is found in one of the chain objects (or in one of its prototypes), or until the chain of visibility areas ends.

The operation on the identifier occurs in the same way as expressions for accessing the properties of objects are used, as described above. An object identified in the scope chainas having the corresponding property, takes the place of the object in the expression of access to the property, and the identifier acts as the name of the property of this object. The global object is always at the end of the scope chain.

Since the execution context of the function call contains the activation object (variable object) at the beginning of the chain, the identifiers used in the function body first check whether they correspond to formal parameters, the names of internal function declarations or local variables. They will be computed as named properties of the activation object (variable object).

The translation also involved Lyudmila Likhovid.

Closures in Javascript [Part 2]

Also popular now: