JavaScript is a programming language that is widely used in web development. It is known for its ability to dynamically modify and process the content of a web page. One of the important aspects of the JavaScript language is scope and closures, that allow programmers to manipulate different elements in the program code efficiently.
A scope is the accessibility zone of code elements. It can be global or local.
Global scope implies that program elements are open for use in any part of the program. They are declared outside functions and blocks.
let breed = "Doberman";
function BreedOfDog() {
console.log(`Breed of dog: ${breed}.`);
}
BreedOfDog();
In the example above, breed
is a global variable. It can be used in any part of the program, including the BreedOfDog()
function. When the latter is called, breed
is used to output the dog's breed.
However, using global variables can lead to problems. Consider the example below:
let breed = "Doberman";
function BreedOfDog() {
console.log(`Breed of dog: ${breed}.`);
}
function changeTheBreed() {
breed = "Labrador";
}
BreedOfDog();
changeTheBreed();
BreedOfDog();
Here, changeTheBreed()
changes the breed
value, and as a result, BreedOfDog()
outputs a different breed of dog with a breed
value of "Labrador"
. This behavior can cause confusion in the code and make it difficult to track variable usage.
Local scope means elements are available only in a specific block or function. It helps avoid conflicts between elements declared in different parts of the program. There are two types of local scope: function scope and block scope.
Function scope is the scope of elements declared inside a function.
Let's use the code fragment below as an example:
function multiplication(z, x) {
var res = z * x;
return res;
}
console.log(multiplication(4, 5)); // 20
console.log(res); // Error
Here, res
is declared inside multiplication()
, so it will only be available within that function. If you try to access res
outside of it, JavaScript will return a ReferenceError
.
Block scope is the scope of variables declared within a block of code. It can be any code block enclosed in curly braces, such as an if
conditional statement or a for
loop.
Let's use the code below as an example:
function calculatePrice(quantity) {
let price = 100;
if (quantity > 10) {
let discount = 0.1;
price = price - (price * discount);
}
console.log("Total price: " + price);
console.log("Discount: " + discount);
}
calculatePrice(15);
Here, we define the calculatePrice()
function. It takes the quantity of goods as an argument. Two variables are defined inside it: price
and discount
; price
is initialized to 100
.
Then we use the if
block construct to check if the quantity of goods is greater than 10
. If it is, we create a new discount
variable and set its value to 0.1
, which means a 10% discount. Then we change the price
value to account for this discount.
Finally, we display the total purchase price and discount. However, if we try to refer to discount
outside of the if
construct, as in our example, we get an error.
Function hoisting is a mechanism that allows a function to be available before it has been declared in code. However, only function declarations are hoisted, not their definitions.
Let's look at an example:
let breed = "Doberman";
BreedOfDog();
function BreedOfDog() {
console.log(`Breed of dog: ${breed}.`);
}
In this case, BreedOfDog()
will be hoisted before it is called, so the code will run without errors.
Declaring a variable and assigning it to a function as an expression is also possible. Then, no hoisting will take place.
let breed = "Doberman";
BreedOfDog(); // TypeError: BreedOfDog is not a function
var BreedOfDog = function() {
console.log(`Breed of dog: ${breed}.`);
}
The code above will not work and will cause errors because the function is called before it is initialized.
Every function has its own unique scope. Therefore, it cannot access elements declared in another function unless they have been passed as arguments.
Here is an example:
example2();
function example1() {
var var1 = "Secret message for example1";
}
function example2() {
var var2 = "Secret message for example2";
console.log(var2);
console.log(var1);
}
In the example above, var1
is not available for example2()
. Therefore, an error will appear when console.log(var1)
is called.
Nested scope means one scope is inside another. This means that the inner scope can access elements declared by the outer scope. This rule won't work the other way.
For clarity, see the example below:
example1();
function example1() {
var var1 = "Secret message for example1";
function example2() {
var var2 = "Secret message for example2";
console.log(var1);
}
example2(); // Secret message for example1
console.log(var2); // ReferenceError: var2 is not defined
}
Here, example2()
has access to the variable var1
, which is defined for example1()
. However, example1()
does not have access to var2
, which is defined for example2()
. If you try to access var2
from example1()
, a ReferenceError
will be called. This is because var2
is in the scope of example2()
and cannot be accessed from code elements.
Closures are a mechanism for handling scopes in JavaScript that allows you to retain access to variables even after the function in which those variables were declared has been terminated.
function breed() {
var nameOfBreed = "Doberman";
return function BreedOfDog() {
console.log(`Breed of dog: ${nameOfBreed}.`);
}
}
var dog = breed();
dog();
In the example above, breed()
returns BreedOfDog()
, which remembers the value of the nameOfBreed
variable at the time it was defined. We then pass the breed()
call to the dog
variable. We then call dog()
, and it uses the stored value of nameOfBreed
to output the string "Breed of dog: Doberman"
.
Closures are used to accomplish different tasks. For example, to control side effects, or to create private variables.
A side effect is a change of the program state outside the function. For example, when a function changes the value of a variable outside its scope, this would be a side effect. Side effect control implies that functions should not change program state outside their scope. Instead, they should return values that may be needed in other parts of the program.
function createCounter() {
let count = 1;
function increment() {
count *= 2;
return count;
}
return increment;
}
const counter = createCounter();
console.log(counter()); // 2
console.log(counter()); // 4
console.log(counter()); // 8
In the example above, createCounter()
returns a nested increment()
function that modifies count
within its scope. After createCounter()
has been called, a closure is created that stores count
in memory and returns a reference to increment()
. Thus, at the time counter()
is called, the value of count
is changed and the new value is returned.
Closures are also used for the purpose of creating private variables and methods. Private variables are variables that are only available inside a function. This can be useful when you want to hide some information or protect it from being changed.
In the example from the last section, the count
variable was declared, which is private because it is not accessible from the outside and cannot be changed directly.
While developing JavaScript applications, you may encounter problems with scopes. A tool that can help you easily track and debug issues is DevTools.
Here are some ways to use DevTools:
Breakpoints
Breakpoints are places in the code where script execution will stop so that you can analyze the current state of the scope.
To set a breakpoint, click on the line number in the code editor in DevTools. Once the breakpoint is set, you can run the code, and the script execution will stop on the specified line.
As you can see in the image above, we have set the breakpoints successfully. It is indicated by the blue highlighting of the code line number and the list of breakpoints in the right "Breakpoints" menu.
The debugger
keyword
The debugger
keyword is an instruction that, when executed, enables the JavaScript debugger in the browser.
To use debugger
, you must insert it into your code:
function greet(name) {
let greeting = "Hello";
console.log(`${greeting}, ${name}!`);
debugger;
console.log("Done!");
}
greet("John");
When the browser executes this code and reaches the debugger
instruction, it stops, and DevTools automatically opens in the "Sources" tab with the current cursor location, as shown below.
Watch expressions
Watch expressions are expressions that you can add to DevTools to track variable values in real time.
To add an expression, click the "Add Watch Expression" button in the Sources pane of DevTools and enter the expression you want to watch.
For example, if you want to track the value of the name
variable in real time, you can add the following expression:
This will allow you to track real-time changes in the values of the variables.
This article has talked about scopes and closures in JavaScript. When done correctly, they allow programmers to create safe and manageable code that can be easily read and maintained. We also learned how to DevTools to help track and debug problems related to scopes.