Hey there! Welcome to Hostman! 🎉

Scope and Closures in JavaScript

27.11.2023
Reading time: 8 min
Hostman Team
Technical writer

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.

Scope: global and local

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

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

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.

F1d46c67 065e 45a5 9fb7 8ea0797ef608

Function hoisting

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.

A25b75bc 35ee 42cf A415 7e4b1f3a682d

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.

8f33beb5 A686 48fc 9518 7b01bf5bd139

Limiting the scope of different functions

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.

962b0d6c F36d 4e59 8c38 6854fdf1ef5c

Nested scope

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

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.

Controlling side effects

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. 

Private variables

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.

Checking scopes with DevTools

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.

E755826c 8a71 4793 9478 33ca43dda12c

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.

11e1ae1c 70fc 4437 B605 41ee1651fd32

  • 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:

4ae32649 365c 48ed B489 46d9cfa3c323

This will allow you to track real-time changes in the values of the variables.

Conclusion

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.