In this chapter, we will continue to look at how variables work, and, as a result, we will become familiar with closures. From the global object, we turn to working inside functions.
Lexical environment
All variables inside a function are properties of a special LexicalEnvironment
internal object.
We will call this object “lexical environment” or simply “variable object”.
At startup, the function creates a LexicalEnvironment
object, writes there arguments, functions, and variables. The initialization process is performed in the same order as for the global object, which, generally speaking, is a special case of the lexical environment.
Unlike window
, the LexicalEnvironment
object is internal, it is hidden from direct access.
Example
Let's take an example to better understand how this works:
2 | var phrase = "Привет, " + name; |
When calling a function:
- Before the first line of its code is executed, at the initialization stage, the interpreter creates an empty
LexicalEnvironment
object and fills it. In this case, the name
argument and a single variable phrase
get there:
3 | var phrase = "Привет, " + name; |
- The function is executed.
At runtime, the local variable phrase
is assigned, that is, in other words, the new value is assigned to the LexicalEnvironment.phrase
property:
3 | var phrase = "Привет, " + name; |
- At the end of the function, the object with variables is usually thrown away and the memory is cleared.
If you read the ECMA-262 specification, then we will see that we are talking about two objects: VariableEnvironment
and LexicalEnvironment
.
But it is also noted there that in implementations these two objects can be combined. So we avoid unnecessary details and use the term LexicalEnvironment
everywhere, it quite accurately allows us to describe what is happening.
A more formal description is found in the ECMA-262 specification, sections 10.2-10.5 and 13.
Access to external variables
From the function, we can access not only a local variable, but also an external one:
The interpreter, when accessing a variable, first tries to find a variable in the current LexicalEnvironment
, and then, if it does not exist, searches for an external variable object. In this case, it is a window
.
This search order is possible due to the fact that the reference to the external object of variables is stored in a special internal property of the function, which is called [[Scope]]
.
Consider how it is created and used in the code above:
- It all starts with the creation of the function. The function is not created in a vacuum, but in a certain lexical environment.
In the case of the above function is created in the global lexical environment window
:
In order for the function to access external variables in the future, at the time of its creation, it receives the hidden [[Scope]]
property, which refers to the lexical environment in which it was created:
This link appears simultaneously with the function and dies with it. The programmer cannot get or change it in any way.
- Later, the time comes and the function starts.
The interpreter recalls that it has the f.[[Scope]]
property:
... And uses it when creating an object of variables for a function.
A new LexicalEnvironment
object receives a reference to an “external lexical environment” with a value from [[Scope]]
. This link is used to search for variables that are not in the current function.
For example, alert(a)
first searches for variables in the current object: it is empty. And then, as shown by the green arrow in the figure below - by reference, in the external environment.
At the code level, it looks like a search in the external scope, outside the function:
To summarize:
- When creating, each function receives a
[[Scope]]
link to an object with variables in the context of which it was created. - When the function starts, a new object is created with variables. It copies the link to an external object from
[[Scope]]
. - When searching for variables, it is performed first in the current variable object, and then via this link. Due to this, external variables are available in the function.
Importance: 5
The result is true
, because var
will be processed and the variable will be created before the code is executed.
Accordingly, the assignment of value=true
will work on a local variable, and alert
will output true
.
The external variable does not change.
PS If var
not present, then the variable will not be found in the function. The interpreter will call for it in a window
and change it there.
So without var
result will also be true
, but the external variable will change.
[Open task in new window]
Importance: 5
The result will be undefined
, then 5
.
The var
directive will be processed prior to the execution of the function code. A local variable will be created, i.e. LexicalEnvironment
property:
When the execution of the code begins and the alert
triggers, it will output a local variable.
Then assignment will work, and the second alert
will print 5
already.
[Open task in new window]
Importance: 4
The result is a mistake . Try:
The fact is that after var a = 5
there is no semicolon.
JavaScript sees this code as if there were no line breaks:
That is, it tries to call function 5
, which leads to an error.
If you put a semicolon, everything will be fine:
This is one of the most frequent and dangerous pitfalls, leading to the mistakes of those who do not put a semi-colon.
[Open task in new window]
Nested functions
Inside the function, you can declare not only local variables, but also other functions.
As a rule, this is done for auxiliary operations, for example, in the code below, makeMessage
and getHello
are used to generate a message:
Nested functions can be declared both as Function Declaration
and as Function Expression
.
Nested functions are processed in exactly the same way as global ones. The only difference is that they are created in the variable object of the external function, and not in the window
.
That is, when you start the external sayHi
function, local variables and nested Function Declaration
fall into its LexicalEnvironment
. At the same time, the Function Declaration
immediately ready for execution.
In the example above, when launching sayHi(person)
, the following LexicalEnvironment
will be created:
2 | person: переданный аргумент, |
4 | getHello: function ..., |
5 | makeMessage: function ... |
Then, during the execution of sayHi
, nested functions can be accessed; they will be taken from the local variable object.
The nested function has access to external variables through [[Scope]]
.
- When creating any function, including a nested function, it gets the
[[Scope]]
property, indicating the variable object in which it was created. - When launched, it will search for variables first in itself, then in an external variable object, then in a more external one, and so on.
Therefore, in the example above, the makeMessage(person)
argument can be removed from the declaration of the function makeMessage(person)
.
It was:
1 | function sayHi(person) { |
3 | function makeMessage(person) { |
4 | return getHello(person.age) + ', ' + person.name; |
Will become:
1 | function sayHi(person) { |
3 | function makeMessage() { |
5 | return getHello(person.age) + ', ' + person.name; |
Nested function can be returned.
For example, suppose sayHi
does not issue an alert
right there, but returns a function that does this:
In real life, this is needed to call the resulting function later when it is needed. For example, when you press a button.
The returned function (*)
will have full access to the arguments of the external function as well as to the other nested functions makeMessage
and getHello
, because it receives the [[Scope]]
link, which points to the current LexicalEnvironment
. Variables that are not in it, for example, person
, will be taken from it.
In particular, the function makeMessage
when called in the (**)
string will be taken from an external variable object.
External LexicalEnvironment
, in turn, may refer to even more external LexicalEnvironment
, and so on.
The closure of the function is called this function itself, plus the entire chain of LexicalEnvironment
, which is formed.
Sometimes they say "the variable is taken from the circuit." This means from an external variable object.
It can be said in another way: “the closure is a function and all external variables that are accessible to it”.
Memory management
JavaScript is designed so that any object and, in particular, a function, exists as long as there is a link to it, while it is somehow available to be called, accessed. For more information about memory management, see the article Memory Management in JS and DOM.
This implies an important consequence when working with closures.
The variable object of an external function exists in memory as long as there is at least one internal function that references it through the [[Scope]]
property.
Let's look at examples.
- Normally, the variable object is deleted when the function ends. Even if it has an internal function declaration: In the above code, the internal function is declared, but it remains inside. After the end of the
f()
operation, it will become unavailable for calls, so it will be removed from memory along with the other local variables. - ... But in this case, the lexical environment, including the variable
a
, will be saved: - If
f()
is called many times, and the resulting functions are saved, for example, added to an array, then LexicalEnvironment
objects with the corresponding values of a
will be saved: 4 | return function () { }; |
9 | var arr = [f(), f(), f()]; |
Note that the variable a
not used in the return function. This means that the browser optimizer can “de facto” delete it from memory. Anyway, because no one will notice. - In this code, the closure is first stored in memory, and after removing the reference to
g
dies: 02 | var a = Math.random(); |
[[Scope]]
for new Function
There is one exception to the general assignment rule [[Scope]]
.
There is another way to create a function, which we have not talked about before, because it is used very rarely. It looks like this:
That is, the function is created by calling new Function(params, code)
:
-
params
- Function parameters separated by commas as strings.
-
code
- Function code as a string.
This method is used very rarely, but in some cases it is very useful, as it allows you to construct a function during program execution, for example, from data received from a server or from a user.
When creating a function using new Function
, its [[Scope]]
property refers not to the current LexicalEnvironment
, but to the window
.
The following example demonstrates how the function created by new Function
ignores external variable a
and outputs global instead.
First, the usual behavior:
And now for the function created through the new Function
:
Total
- All variables and parameters of functions are properties of the variable object
LexicalEnvironment
. Each function launch creates a new such object.
At the top level, the “global object” plays the role of LexicalEnvironment
, in the browser it is a window
. - When created, the function gets the
[[Scope]]
system property, which refers to the LexicalEnvironment
in which it was created (except for new Function
). - When the function starts, its
LexicalEnvironment
refers to the external one stored in [[Scope]]
. Variables are first searched in their object, then in the object by reference, and so on, down to the window
.
Developing without closures in JavaScript is almost impossible. You will meet them more than once in the next chapters of the textbook.
Comments
To leave a comment
Scripting client side JavaScript, jqvery, BackBone
Terms: Scripting client side JavaScript, jqvery, BackBone