📜 ⬆️ ⬇️

Closures in javascript for beginners

Closures are one of the fundamental concepts of JavaScript, causing difficulties for many beginners to know and understand that every JS programmer should. Having dealt well with closures, you will be able to write better, more efficient, and cleaner code. And this, in turn, will contribute to your professional growth.

The material, the translation of which we publish today, is dedicated to the story about the internal mechanisms of closures and how they work in JavaScript-programs.


What is a closure?


A closure is a function that has access to the scope, the function that is external to it, even after this external function has completed its work. This means that the variables declared in the external function and the arguments passed to it can be stored in the closure. Before we move on to closures, let's deal with the concept of "lexical environment".
')

What is the lexical environment?


The term "lexical environment" or "static environment" in JavaScript refers to the ability to access variables, functions and objects based on their physical location in the source code. Consider an example:

let a = 'global';  function outer() {    let b = 'outer';    function inner() {      let c = 'inner'      console.log(c);   // 'inner'      console.log(b);   // 'outer'      console.log(a);   // 'global'    }    console.log(a);     // 'global'    console.log(b);     // 'outer'    inner();  } outer(); console.log(a);         // 'global' 

Here, the inner() function has access to variables declared in its own scope, in the scope of the outer() function, and in the global scope. The outer() function has access to variables declared in its own scope and global scope.

The chain of scopes of the above code will look like this:

 Global { outer {   inner } } 

Notice that the inner() function is surrounded by the lexical environment of the outer() function, which, in turn, is surrounded by the global scope. That is why the inner() function can access variables declared in the outer() function and in the global scope.

Practical examples of closures


Consider, before analyzing the intricacies of the internal structure of closures, a few practical examples.

â–ŤSample number 1


 function person() { let name = 'Peter'; return function displayName() {   console.log(name); }; } let peter = person(); peter(); // 'Peter' 

Here we call the person() function, which returns the internal displayName() function, and store this function in the peter variable. When we then call the peter() function (the corresponding variable actually stores the reference to the displayName() function), the name Peter is displayed in the console.

At the same time, the displayName() function does not have a variable named name , so we can conclude that this function can somehow access the variable declared in the function external to it, person() , even after how this function worked. Perhaps this is because the displayName() function is actually a closure.

â–ŤSample number 2


 function getCounter() { let counter = 0; return function() {   return counter++; } } let count = getCounter(); console.log(count());  // 0 console.log(count());  // 1 console.log(count());  // 2 

Here, as in the previous example, we store the reference to the anonymous internal function returned by the getCounter() function in the variable count . Since the count() function is a closure, it can access the counter variable of the getCount() function even after the getCounter() function has completed its work.

Note that the value of the counter variable is not reset to 0 each time the count() function is called. It may seem that it should be reset to 0, as it could be when calling a normal function, but this does not happen.

Everything works this way because every time the count() function is called, a new scope is created for it, but there is only one scope for the getCounter() function. Since the counter variable is declared in the scope of the getCounter() function, its value between count() function calls is preserved, not resetting to 0.

How do closures work?


So far we have talked about closures, and considered practical examples. Now let's talk about the internal mechanisms of JavaScript, ensuring their work.

In order to understand closures, we need to deal with the two most important concepts of JavaScript. This is the execution context and the lexical environment.

Context of execution


The execution context is an abstract environment in which the JavaScript code is computed and executed. When global code is executed, it occurs within the global execution context. The function code is executed inside the execution context of the function.

At some point in time, the code can be executed only in one execution context (JavaScript is a single-threaded programming language). These processes are managed using the so-called Call Stack.

The call stack is a data structure based on the LIFO principle (Last In, First Out - last entered, first out). New elements can be placed only in the upper part of the stack, and only elements can be removed from it.

The current execution context will always be at the top of the stack, and when the current function terminates, its execution context is retrieved from the stack and control is transferred to the execution context, which was located below the context of this function in the call stack.

Consider the following example in order to better understand what the execution context and call stack are:


Execution context example

When this code is executed, the JavaScript engine creates a global execution context for executing global code, and when it encounters a function call first() , it creates a new execution context for this function and places it on top of the stack.

The call stack for this code looks like this:


Call stack

When the execution of the first() function is completed, its execution context is retrieved from the call stack and control is transferred to the execution context that is below it, that is, the global context. After that, the code remaining in the global scope will be executed.

â–ŤLexic environment


Every time the JS engine creates an execution context for executing a function or a global code, it also creates a new lexical environment for storing variables declared in this function during its execution.

A lexical environment is a data structure that stores information about the correspondence of identifiers and variables. Here, “identifier” is the name of a variable or function, and “variable” is a reference to an object (functions are also included) or a value of a primitive type.

The lexical environment contains two components:


Conceptually, the lexical environment looks like this:

 lexicalEnvironment = { environmentRecord: {   <identifier> : <value>,   <identifier> : <value> } outer: < Reference to the parent lexical environment> } 

Take a look at the following code snippet:

 let a = 'Hello World!'; function first() { let b = 25;  console.log('Inside first function'); } first(); console.log('Inside global execution context'); 

When the JS engine creates a global execution context for executing global code, it also creates a new lexical environment for storing variables and functions declared in the global scope. As a result, the lexical environment of the global scope will look like this:

 globalLexicalEnvironment = { environmentRecord: {     a : 'Hello World!',     first : < reference to function object > } outer: null } 

Note that the reference to the external lexical environment ( outer ) is set to null , since the global scope does not have an external lexical environment.

When the engine creates an execution context for the first() function, it also creates a lexical environment for storing variables declared in this function during its execution. As a result, the lexical environment of the function will look like this:

 functionLexicalEnvironment = { environmentRecord: {     b : 25, } outer: <globalLexicalEnvironment> } 

The link to the external lexical environment of the function is set to <globalLexicalEnvironment> , since in the source code the function code is in the global scope.

Note that when a function completes, its execution context is retrieved from the call stack, but its lexical environment can be removed from memory, or it can remain there. It depends on whether there are references to this lexical environment in other lexical environments in the form of references to an external lexical environment.

Detailed analysis of examples of working with closures


Now that we have armed ourselves with knowledge of the execution context and the lexical environment, let us return to the closures and more deeply analyze the same code fragments that we have already considered.

â–ŤSample number 1


Take a look at this code snippet:

 function person() { let name = 'Peter'; return function displayName() {   console.log(name); }; } let peter = person(); peter(); // 'Peter' 

When the person() function is executed, the JS engine creates a new execution context and a new lexical environment for this function. Completing the work, the function returns the displayName() function, a reference to this function is written to the peter variable.

Her lexical environment will look like this:

 personLexicalEnvironment = { environmentRecord: {   name : 'Peter',   displayName: < displayName function reference> } outer: <globalLexicalEnvironment> } 

When the person() function completes, its execution context is retrieved from the stack. But its lexical environment remains in memory, since the link to it is in the lexical environment of its internal function displayName() . As a result, variables declared in this lexical environment remain accessible.

When the peter() function is called (the corresponding variable stores a reference to the displayName() function), the JS engine creates a new execution context and a new lexical environment for this function. This lexical environment will look like this:

 displayNameLexicalEnvironment = { environmentRecord: {   } outer: <personLexicalEnvironment> } 

The displayName() function has no variables, so its environment record will be empty. During the execution of this function, the JS engine will try to find the name variable in the lexical environment of the function.

Since the displayName() function cannot be found in the lexical environment, the search will continue in the external lexical environment, that is, in the lexical environment of the person() function, which is still in memory. There, the engine finds the desired variable and displays its value in the console.

â–ŤSample number 2


 function getCounter() { let counter = 0; return function() {   return counter++; } } let count = getCounter(); console.log(count());  // 0 console.log(count());  // 1 console.log(count());  // 2 

The lexical environment of the getCounter() function will look like this:

 getCounterLexicalEnvironment = { environmentRecord: {   counter: 0,   <anonymous function> : < reference to function> } outer: <globalLexicalEnvironment> } 

This function returns an anonymous function that is assigned to the variable count .

When the count() function is executed, its lexical environment looks like this:

 countLexicalEnvironment = { environmentRecord: { } outer: <getCountLexicalEnvironment> } 

When executing this function, the system will search for the counter variable in its lexical environment. In this case, again, the record of the function environment is empty, so the search for the variable continues in the external lexical environment of the function.

The engine finds the variable, displays it in the console and increments the counter variable stored in the getCounter() function's lexical environment.

As a result, the lexical environment of the getCounter() function after the first call of the count() function will look like this:

 getCounterLexicalEnvironment = { environmentRecord: {   counter: 1,   <anonymous function> : < reference to function> } outer: <globalLexicalEnvironment> } 

Each time you call the count() function, the JavaScript engine creates a new lexical environment for this function and increments the counter variable, which leads to changes in the lexical environment of the getCounter() function.

Results


In this article, we talked about what closures are and dismantled the underlying JavaScript mechanisms underlying them. Closures are one of the most important fundamental concepts of JavaScript; every JS developer should understand it. Understanding closures is one step in the path to writing efficient and high-quality applications.

Dear readers! If you have experience in JS development, please share practical examples of how to use closures with beginners.

Source: https://habr.com/ru/post/424967/


All Articles