📜 ⬆️ ⬇️

JavaScript scopes

In JavaScript, the scope is an important, but ambiguous concept. Scopes, with the right approach to their use, allow the use of robust design patterns that help avoid unwanted side effects in programs. In this article we will analyze the different types of scopes in JavaScript, let's talk about how they work. A good understanding of this mechanism will allow you to improve the quality of the code.


Image for request "scopes". Sorry if you caused a nostalgia)

An elementary definition of the scope is as follows: this is the area where the compiler searches for variables and functions when it needs them. Think it sounds too easy? We offer to understand together.

Javascript interpreter


Before talking about scopes, you need to discuss the JavaScript interpreter, consider how it affects different scopes. When executing a JS code, the interpreter passes through it twice.
')
The first pass through the code, also called the compilation pass, is the one that most strongly affects the scopes. The interpreter scans the code for declarations of variables and functions and raises these declarations to the top of the current scope. It is important to note that only variable declarations are raised, and the assignment operations remain as they are - for the next pass, called the performance pass.

In order to better understand this, consider a simple code snippet:

'use strict' var foo = 'foo'; var wow = 'wow'; function bar (wow) {  var pow = 'pow';  console.log(foo); // 'foo'  console.log(wow); // 'zoom' } bar('zoom'); console.log(pow); // ReferenceError: pow is not defined 

This code, after compilation, will look something like this:

 'use strict' //         var foo; var wow; //    ,   ,       function bar (wow) { var pow; pow = 'pow'; console.log(foo); console.log(wow); } foo = 'foo'; wow = 'wow'; bar('zoom'); console.log(pow); // ReferenceError: pow is not defined 

Here we must pay attention to the fact that the ads rise to the top of their current scope. This, as will be seen below, is very important for understanding the scopes in JavaScript.

For example, the variable pow was declared in the bar function, since this is its scope. Note that the variable is not declared in the parent, with respect to the function, scope.

The wow parameter of the bar function is also declared in the function scope. In fact, all the parameters of the function are implicitly declared in its scope, and that is why the console.log(wow) command in the ninth line, inside the function, displays the zoom instead of wow .

Lexical scope


Having considered the features of the JavaScript interpreter and addressing the topic of raising variables and functions, we can proceed to talk about the scope. Let's start with the lexical scope. It can be said that this is a scope that is formed at compile time. In other words, the decision on the boundaries of this scope is made at compile time . For the purposes of this article, we will ignore the exceptions to this rule that occur in code that uses eval or with commands. We believe that these commands, in any case, should not be used.

The second pass of the interpreter is the one during which values ​​are assigned to variables and functions are executed. In the above code example, it is during this pass that the bar() call is performed in line 12.

The interpreter needs to find the declaration bar before making this call, it does it, starting with the search in the current scope. At that moment, the current is the global scope. Thanks to the first pass, i.e. compilation, we know that the bar declaration is at the top of the code, so the interpreter can find it and perform the function.

If we look at line 8, where is the console.log(foo); command console.log(foo); , the interpreter, before executing this command, will need to find the declaration foo . The first thing he does, again, is looking for the current scope, which at this moment is the scope of the function bar , and not the global scope. Is the variable foo declared in the function scope? No, it is not. It then goes up a level, to the parent scope, and looks for a variable declaration there. The scope in which the function is declared is the global scope. Is the variable foo declared in the global scope? Yes it is. Therefore, the interpreter can take the value of a variable and execute the command.

In general, we can say that the meaning of the lexical scope is that the scope is determined after compilation, and when the interpreter needs to find a declaration of a variable or function, it first looks in the current scope, but if it finds what it needs, fails, it goes into the parent scope, continuing the search on the same principle. The highest level to which it can go is called the global scope.

If what the interpreter is looking for is not in the global scope, it will generate a ReferenceError error.

In addition, since the interpreter first searches for what it needs in the current scope, and only then in the parent, the lexical scope introduces the concept of shading variables in JavaScript. This means that the variable foo declared in the current scope of the function will shade or hide the variable with the same name declared in the parent scope. Take a look at the following example in order to better understand this idea:

 'use strict' var foo = 'foo'; function bar () { var foo = 'bar'; console.log(foo); } bar(); 

This code will print the string bar in the console, not foo , since declaring the variable foo in the sixth line will block the declaration of the variable with the same name in the third line.

Variable shading is a design pattern that can be useful if you want to mask some variables and prevent them from being accessed from specific scopes. I must say that I usually avoid using this technique, applying it only if it is absolutely impossible to do without it, as I am sure that using the same variable names leads to confusion in team development. The use of shading can lead to the fact that the developer can decide that the variable does not store what is actually in it.

Functional scope


As we have seen, when considering the lexical scope, the interpreter declares variables in the current scope, which means that the variables declared in the function are declared in the functional scope. This scope is limited to the function itself and its descendants — other functions declared inside this function.

Variables declared in the functional scope cannot be accessed externally. This is a very powerful design pattern that you can use if you want to create private properties and have access to them only inside the functional scope. Here's what it looks like:

 'use strict' function convert (amount) {  var _conversionRate = 2; //        return amount * _conversionRate; } console.log(convert(5)); console.log(_conversionRate); // ReferenceError: _conversionRate is not defined 

Block scope


Block scope is similar to functional, but it is not limited to a function, but a block of code.

In ES3, the catch expression in the try / catch construct has a block scope, which means that this expression has its own scope. It is important to note that the try expression does not have a block scope, only the catch expression has it. Consider an example:

 'use strict' try { var foo = 'foo'; console.log(bar); } catch (err) { console.log('In catch block'); console.log(err); } console.log(foo); console.log(err); 

This code will generate an error on the fifth line when we try to access bar , which causes the interpreter to go to the catch expression. In the scope of the expression declared variable err , which will not be accessible from the outside. In fact, the error will be displayed when we try to output the value of the variable err to the log in the line console.log(err) This is what this code will output:

 In catch block ReferenceError: bar is not defined   (...Error stack here...) foo ReferenceError: err is not defined (...Error stack here...) 

Note that the variable foo is available outside the try / catch construct, and err is not.

If we talk about ES6, then using the let and const keywords, variables and constants implicitly join the current block scope instead of the functional scope. This means that these constructs are limited to the block in which they are used, whether it be an if block, a for block, or a function. Here is an example that will help to better understand this:

 'use strict' let condition = true; function bar () { if (condition) {   var firstName = 'John'; //     let lastName = 'Doe'; //     if   const fullName = firstName + ' ' + lastName; //     if } console.log(firstName); // John console.log(lastName); // ReferenceError console.log(fullName); // ReferenceError } bar(); 

The let and const keywords allow us to use the principle of least disclosure. Following this principle means that the variable must be available in the smallest possible scope. Prior to ES6, developers often achieved a blocky scope effect using the stylistic method of declaring variables with the var keyword in an instantly executed functional expression (Immediately Invoked Function Expression, IIFE), but now, thanks to let and const , you can apply a functional approach. Some of the main advantages of this principle are to avoid unwanted access to variables, and thus reduce the likelihood of errors. In addition, it allows the garbage collector to free memory from unnecessary variables when leaving the block scope.

Immediately executable function expressions


IIFE is a very popular JavaScript design pattern that allows functions to create a new block scope. IIFE are ordinary functional expressions that we execute immediately after they are processed by the interpreter. Here is an example of IIFE:

 'use strict' var foo = 'foo'; (function bar () { console.log('in function bar'); })() console.log(foo); 

This code will print the line in function bar before the output of foo , since the bar function is executed immediately, without the need to explicitly call it using a bar() . This happens for the following reasons:


As we have already seen, this allows you to hide variables from code from external scopes, to restrict access, and in order not to pollute external scopes with unnecessary variables.

IIFE is also very useful if you are performing an asynchronous operation and want to save the state of variables in the IIFE scope. Here is an example of this behavior:

 'use strict' for (var i = 0; i < 5; i++) { setTimeout(function () {   console.log('index: ' + i); }, 1000); } 

It can be expected that this code will output 0, 1, 2, 3, 4. However, the actual result of the execution of this for loop, in which the asynchronous setTimeout operation is called, will look like this:

 index: 5 index: 5 index: 5 index: 5 index: 5 

The reason for this is that by the time 1000 milliseconds have elapsed, the execution of the for loop will end and counter i will be equal to 5.

In order for the code to work as expected, it would output a sequence of numbers from 0 to 4, we need to use IIFE to preserve the required scope:

 'use strict' for (var i = 0; i < 5; i++) { (function logIndex(index) {   setTimeout(function () {     console.log('index: ' + index);   }, 1000); })(i) } 

In this example, we pass the value of i to IIFE. A functional expression will have its own scope, which is no longer affected by what happens in the for loop. This is what this code will output:

 index: 0 index: 1 index: 2 index: 3 index: 4 

Results


We looked at various scopes in JavaScript, talked about their features, described some simple design patterns. In fact, you can still talk and talk about areas of visibility in JavaScript, but I believe that this material provides a good basis, using which you will be able to deepen and expand your knowledge on your own.

I hope this story has helped you better understand the scope in JavaScript, and thus improve the quality of your programs. We can also recommend for reading this publication on Habré.

Dear JS developers! Please share some interesting techniques for working with scopes in JavaScript.

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


All Articles