Closures in javascript for beginners

Original author: Sukhjinder Arora
  • Transfer
  • Tutorial
Closures are one of the fundamental concepts of JavaScript, causing difficulties for many beginners to know and understand that every JS programmer should. Well dealt with closures, you can 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 proceed, in fact, to the 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';
  functionouter() {
    let b = 'outer';
    functioninner() {
      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 function inner()has access to variables declared in its own scope, in the scope of the function, outer()and in the global scope. The function outer()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 function inner()is surrounded by the lexical environment of the function outer(), which, in turn, is surrounded by the global scope. That is why the function inner()can access variables declared in the function outer()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


functionperson() {
  let name = 'Peter';
  
  returnfunctiondisplayName() {
    console.log(name);
  };
}
let peter = person();
peter(); // 'Peter'

Here we call a function person()that returns an internal function displayName(), and store this function in a variable peter. When we call the function after this peter()(the corresponding variable actually stores the reference to the function displayName()), the name is output to the console Peter.

At the same time, displayName()there is no variable with the name in the function name, so we can conclude that this function can somehow access the variable declared in the function external to it person(), even after this function has completed. Perhaps this is because the function displayName()is in fact a closure.

▍Sample number 2


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

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

Note that the value of a variable is counternot reset to 0 with each function call count(). 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 exactly this way because with each function call count()a new scope is created for it, but there is only one scope for the function getCounter(). Since the variablecounterdeclared in the scope of the function getCounter(), its value between function calls is count()preserved, without being reset 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 understand the two most important concepts of JavaScript. It 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 items can be placed only at the top of the stack, and only items 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:


Example execution context

When this code is executed, the JavaScript engine creates a global execution context to execute the global code, and when it encounters a function callfirst(), 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 a function is completedfirst(), its execution context is retrieved from the call stack and control is transferred to the execution context 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:

  • The environment record is the place where declarations of variables and functions are stored.
  • Reference to the outer environment — A reference that allows you to access the external (parent) lexical environment. This is the most important component that needs to be dealt with in order to understand closures.

Conceptually, the lexical environment looks like this:

lexicalEnvironment = {
  environmentRecord: {
    <identifier> : <value>,
    <identifier> : <value>
  }
  outer: < Referencetotheparentlexicalenvironment>
}

Take a look at the following code snippet:

let a = 'Hello World!';
functionfirst() {
  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 tofunctionobject >
  }
  outer: null
}

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

When the engine creates an execution context for a function first(), it 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 a value <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 links 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:

functionperson() {
  let name = 'Peter';
  
  returnfunctiondisplayName() {
    console.log(name);
  };
}
let peter = person();
peter(); // 'Peter'

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

Her lexical environment will look like this:

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

When a function person()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 a function is called peter()(the corresponding variable stores the function reference displayName()), 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>
}

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

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

▍Sample number 2


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

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

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

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

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

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

When executing this function, the system will search for a variable counterin its lexical environment. In this case, again, the environment record of the function 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 variable counterstored in the lexical environment of the function getCounter().

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

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

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

Results


In this article, we talked about what closures are and explored the underlying JavaScript mechanisms underlying them. Closures is one of the most important fundamental concepts of JavaScript; every JS developer should understand it. Understanding closures is one of the steps 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.


Also popular now: